springsecurity实现图片验证码功能

图片验证码是我们在做登录的时候需求很多的一个功能,其可以帮我们防止恶意登录,保护账户安全。

第一步:maven引入相关依赖

<!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
	<dependency>
		<groupId>com.github.penggle</groupId>
		<artifactId>kaptcha</artifactId>
		<version>2.3.2</version>
		<exclusions>
			<exclusion>
				<artifactId>javax.servlet-api</artifactId>
				<groupId>javax.servlet</groupId>
			</exclusion>
		</exclusions>
	</dependency>

第二步:## 在properties配置文件中做相关的配置

配置验证码

kaptcha.border=no
kaptcha.border.color=105,179,90
kaptcha.image.width=100 #图片宽度
kaptcha.image.height=45 #图片高度
kaptcha.session.key=code #sessio密钥
kaptcha.textproducer.font.color=blue #验证码颜色
kaptcha.textproducer.font.size=35 #验证码字体大小
kaptcha.textproducer.char.length=4 #验证码位数
kaptcha.textproducer.font.names=宋体,楷体,微软雅黑 #验证码字体

第三步:编写一个配置类



import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.util.Properties;

/*
    验证码配置
 */
//加载自定义的配置文件
@Component
public class CaptchaConfig {

    @Value("${kaptcha.border}")
    private String border;

    @Value("${kaptcha.border.color}")
    private String borderColor;

    @Value("${kaptcha.textproducer.font.color}")
    private String fontColor;

    @Value("${kaptcha.image.width}")
    private String imageWidth;

    @Value("${kaptcha.image.height}")
    private String imageHeight;

    @Value("${kaptcha.session.key}")
    private String sessionKey;

    @Value("${kaptcha.textproducer.char.length}")
    private String charLength;

    @Value("${kaptcha.textproducer.font.names}")
    private String fontNames;

    @Value("${kaptcha.textproducer.font.size}")
    private String fontSize;


    @Bean(name = "captchaProducer")
    public DefaultKaptcha getKaptchaBean(){
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();

        Properties properties = new Properties();
        properties.setProperty("kaptcha.border",border);
        properties.setProperty("kaptcha.border.color", borderColor );
        properties.setProperty("kaptcha.image.width",imageWidth  );
        properties.setProperty("kaptcha.textproducer.font.color",fontColor  );
        properties.setProperty("kaptcha.image.height", imageHeight );
        properties.setProperty("kaptcha.session.key",sessionKey  );
        properties.setProperty("kaptcha.textproducer.font.size", fontSize );
        properties.setProperty("kaptcha.textproducer.char.length", charLength );
        properties.setProperty("kaptcha.textproducer.font.names", fontNames );

        defaultKaptcha.setConfig(new Config(properties));

        return defaultKaptcha;
    }

}

该类的成员变量用@Value注解从properties配置文件取值注入
再向容器中注入一个Bean(DefaultKaptcha 类的实例),该实例需要一个Properties集合参数,而Properties集合的键值对信息就是生成验证码的相关配置。

第四步:编写一个验证码过期的配置类



/*
    验证码过期的相关配置
 */

import java.time.LocalDateTime;

public class CaptchaImageVO {

    private String code;

    private LocalDateTime expireTime; //过期时间

    public CaptchaImageVO(){}
    
    public CaptchaImageVO(String code,int expireAfterSeconds){
        this.code = code;
        //当前时间加上指定时间后过期
        this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
    }

    public boolean isExpired(){
        //判断当前时间时候超过设置的时间
        boolean isExpired = LocalDateTime.now().isAfter(expireTime);
        return isExpired;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }
}

提供两个成员方法:
public CaptchaImageVO(String code,int expireAfterSeconds)
该方法参数一是生成的验证码的答案,比如4h16,t8p3,XUOp,这种四位验证码,参数二是设置验证码的失效时间,比如60秒之后过期。
public boolean isExpired()
该方法判断验证码是否过期,如果设定验证码的过期时间超过了LocalDateTime ,now()则说明没有失效,例如:设定的过期时间是1月30号10点,而现在的时间是1.28号10点,这就是没有失效,反之就是失效了。

第五步:编写一个控制器和请求来获取验证码


import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.wk.springcloud.config.auth.imagecode.CaptchaImageVO;
import com.wk.springcloud.config.auth.imagecode.CaptchaUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;

@RestController
public class CaptchaController {

    @Resource(name = "captchaProducer")
    private DefaultKaptcha captchaProducer;

       //todo  切记这里的映射要通知springsecurity允许访问(permitAll())
    @RequestMapping(value = "/kaptcha",method = RequestMethod.GET)
    public void kaptcha(HttpSession session, HttpServletResponse response){
        response.setDateHeader("Expires",0);
        response.setHeader("Cache-Control","no-store, no-cache, must-revalidate");
        response.setHeader("Cache-Control","post-check=0, pre-check=0");
        response.setHeader("Pragma","no-cache");
        response.setContentType("image/jpeg");

        //生成谜底
        String text = captchaProducer.createText();
        //将谜底保存到session中   并设置120秒之后过期
        session.setAttribute(CaptchaUtils.CAPTCHA_SESSION_KEY,new CaptchaImageVO(text,120));

        try {
            ServletOutputStream outputStream = response.getOutputStream();
            //根据谜底生成谜面
            BufferedImage image = captchaProducer.createImage(text);
            //用输出流写入到页面
            ImageIO.write(image,"jpg",outputStream);
            outputStream.flush();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个请求路径要和前端的img的src属性保持一致
在这里插入图片描述
这个类的话就是设置响应对象的一些配置,响应为图片,禁用缓存等等
并创建一个验证码,设置好过期时间后将其放入session中,然后再用输出流返回到前端页面。

第六步:编写验证码的过滤器以及异常处理

mport com.wk.springcloud.config.auth.MyAuthenticationFailureHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;


@Component
public class CaptchaCodeFilter extends OncePerRequestFilter {

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,IOException  {
        if (Objects.equals("/login",request.getRequestURI()) &&
                "post".equalsIgnoreCase(request.getMethod())
                ){

            try {
                //验证谜底与用户输入是否匹配 
                validate(new ServletWebRequest(request));
            } catch (AuthenticationException e) {
               //如果验证有异常 调用登录失败的方法
          myAuthenticationFailureHandler.onAuthenticationFailure(request,response,e);
                filterChain.doFilter(request,response);
                return;
            }

        }
        filterChain.doFilter(request,response);
    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        HttpSession session = request.getRequest().getSession();
        String captchaCode = ServletRequestUtils.getStringParameter(request.getRequest(),"captchaCode");
        if (StringUtils.isEmpty(captchaCode)){
            throw new SessionAuthenticationException("验证码不能为空!");
        }
        //获取session中的验证码
        CaptchaImageVO codeInSession = (CaptchaImageVO)session.getAttribute(CaptchaUtils.CAPTCHA_SESSION_KEY);
        if (Objects.isNull(codeInSession)){
            throw new SessionAuthenticationException("验证码不存在!");
        }

        //校验是否过期
        if (codeInSession.isExpired()){
            //从session中移除
            session.removeAttribute(CaptchaUtils.CAPTCHA_SESSION_KEY);
            throw new SessionAuthenticationException("验证码已经过期!");
        }

        //校验是否匹配
        if (!Objects.equals(codeInSession.getCode(),captchaCode)){
            throw new SessionAuthenticationException("验证码不匹配!");
        }

    }
}

该类继承自OncePerRequestFilter 并重写doFilterInternal方法。
首先我们来看第一个方法 在该过滤器执行时就会调用这个方法
首先请求URI必须是/login,而且是post方式,因为验证码是在登录的请求时才进行校验的,所以这里的URI写的就是系统登录的URI,如果URI不是/login的话,我们就filterChain.doFilter(request,response);放行
接着我们分析一下validate(new ServletWebRequest(request));这个方法
:首先这个方法会拿到session的请求参数captchaCode的值,这个captchaCode是什么?其实就是验证码的input的name属性
这是在这里插入图片描述
这是我们前端的那个验证码输入框。拿到这个请求参数之后,我们先判断一下是否为空,如果为空直接抛出异常,为什么会抛出SessionAuthenticationException这个异常?这个待会说一下。
然后再接着判断session中的验证码是否空,为空还是继续抛出异常,
再继续判断时候过期,如果过期继续抛出异常,再继续判断是否匹配,如果不匹配继续抛出异常。

接着我们看这里,挺关键的
在这里插入图片描述
抛出异常后我们调用了登录失败的处理器,并将异常对象传入,了解security的同学应该知道登录失败可以设置默认的URL或者处理器,这里我们就使用处理器的方式,且抛出的异常刚好是AuthenticationException的子类,这样我们就能顺利捕捉。 接下来我们来看一下这个失败处理器的逻辑。这里记得catch后要return;

package com.wk.springcloud.config.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Value("${spring.security.loginType}")
    private String loginType;

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //todo 判断是否是验证码抛出的异常
        String message = "用户名或密码错误!";
        if (exception instanceof SessionAuthenticationException){
            message = exception.getMessage();
        }

        if ("JSON".equalsIgnoreCase(loginType)){
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(ResponseA.failed().addMsg(message)));
        }else {
            super.onAuthenticationFailure(request, response, exception);
        }
    }
}

首先我们定义了一个message 并判断发生的异常是不是SessionAuthenticationException的实例,如果是的话,说明抛出的是验证码相关异常。否则就是正常登录失败的异常。然后将响应的提示信息相应给前端。

第七步:做最后的配置,通知security可以访问/kaptcha,以及设置验证码过滤器要在登录过滤器之前调用。因为如果验证码校验不通过就不会走登录的逻辑。

在这里插入图片描述
在这里插入图片描述
点击按钮刷新验证码

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果验证码正确而用户名或密码不正确的话
在这里插入图片描述
这样就可以实现该功能了。还有不懂的同学可以评论,看到会第一时间解答。

猜你喜欢

转载自blog.csdn.net/qq_43750656/article/details/104152705