2023-06-18
原文作者:代码有毒 mrcode 原文地址:https://mrcode.blog.csdn.net/article/details/81502532

图片验证码

在登录界面图形添加验证码

  • 开发生成图像验证码接口
  • 在认证流程中加入图像验证码校验
  • 重构代码

开发生成图像验证码接口

思路:
* 根据随机数生成图片
* 将随机数存到session中
* 将生成的图片写入响应中

这里相当于功能,生成图片什么的不记录了。网上一大堆,记录下这里的一些代码思路

由于是公用的,把该服务写在core中

图片验证码信息类

    package cn.mrcode.imooc.springsecurity.securitycore.validate.code;
    
    import java.awt.image.BufferedImage;
    import java.time.LocalDateTime;
    
    /**
     * 图形验证码
     * @author : zhuqiang
     * @version : V1.0
     * @date : 2018/8/3 22:44
     */
    public class ImageCode {
        private BufferedImage image;
        private String code;
        private LocalDateTime expireTime; // 过期时间
    
        /**
         * @param image
         * @param code
         * @param expireIn 过期时间,单位秒
         */
        public ImageCode(BufferedImage image, String code, int expireIn) {
            this.image = image;
            this.code = code;
            this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
        }
        // 是否过期
        public boolean isExpried() {
          return this.expireTime.isBefore(LocalDateTime.now());
      }

验证码服务

    package cn.mrcode.imooc.springsecurity.securitycore.validate.code;
    /**
     * 验证码服务
     * @author : zhuqiang
     * @version : V1.0
     * @date : 2018/8/3 22:48
     */
    @RestController
    public class ValidateCodeController {
        private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
        // 这里又使用了spring的工具类来操作session
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        @GetMapping("/code/image")
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            ImageCode imageCode = createImageCode(request);
            sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
            response.setContentType("image/jpeg");
            //禁止图像缓存。
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Cache-Control", "no-cache");
            ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
        }
    
        private ImageCode createImageCode(HttpServletRequest request) throws IOException {
            String code = RandomStringUtils.randomAlphanumeric(4);
            BufferedImage image = createImageCode(80, 40, code);
            return new ImageCode(image, code, 60);
        }

需要把该服务路径在 cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityConfig 中配置放行。

在认证流程中加入图像验证码校验

步骤:
由之前的源码的探索发现,只要把过滤器添加到spring现有的过滤器链上就可以了;

  1. 编写验证码过滤器
  2. 放在UsernamePasswordAuthenticationFilter过滤器之前
    package cn.mrcode.imooc.springsecurity.securitycore.validate.code;
    
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.social.connect.web.HttpSessionSessionStrategy;
    import org.springframework.social.connect.web.SessionStrategy;
    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 java.io.IOException;
    
    /**
     * 图片验证码验证过滤器
     * OncePerRequestFilter spring提供的,保证在一个请求中只会被调用一次
     * @author : zhuqiang
     * @version : V1.0
     * @date : 2018/8/3 23:24
     */
    public class ValidateCodeFilter extends OncePerRequestFilter {
        // 在初始化本类的地方进行注入
        // 一般在配置security http的地方进行添加过滤器
        private AuthenticationFailureHandler failureHandler;
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 为登录请求,并且为post请求
            if (StringUtils.equals("/authentication/form", request.getRequestURI())
                    && StringUtils.equalsAnyIgnoreCase(request.getMethod(), "post")) {
                try {
                    validate(request);
                } catch (ValidateCodeException e) {
                    failureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
            }
            filterChain.doFilter(request, response);
        }
    
        private void validate(HttpServletRequest request) throws ServletRequestBindingException {
            // 拿到之前存储的imageCode信息
            ServletWebRequest swr = new ServletWebRequest(request);
            ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(swr, ValidateCodeController.SESSION_KEY);
            // 又是一个spring中的工具类,
            // 试问一下,如果不看源码怎么可能知道有这些工具类可用?
            String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
    
            if (StringUtils.isBlank(codeInRequest)) {
                throw new ValidateCodeException("验证码的值不能为空");
            }
            if (codeInSession == null) {
                throw new ValidateCodeException("验证码不存在");
            }
            if (codeInSession.isExpried()) {
                sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
                throw new ValidateCodeException("验证码已过期");
            }
            if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
                throw new ValidateCodeException("验证码不匹配");
            }
            sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
        }
    
        public AuthenticationFailureHandler getFailureHandler() {
            return failureHandler;
        }
    
        public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
            this.failureHandler = failureHandler;
        }
    }

把过滤器添加到现有认证流程中

    ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
    validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
    http
            // 由源码得知,在最前面的是UsernamePasswordAuthenticationFilter
            .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
            // 定义表单登录 - 身份认证的方式
            .formLogin()
            .loginPage("/authentication/require")
            .loginProcessingUrl("/authentication/form")

还需要注意的一个地方就是myAuthenticationFailureHandler中。因为失败会调用这个处理器;
这里和视频中演示的不一样。不会再把异常信息打印到前段页面了。

后补:视频中不知道什么时候把LoginType变成了json类型,所以会抛出异常

    if (securityProperties.getBrowser().getLoginType() == LoginType.JSON) {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.getWriter().write(objectMapper.writeValueAsString(exception));
    } else {
        // 在这里失败跳转不回去了。而且异常信息也没有打印出来。父类默认打印了死的一句话
        // 在这里就不往上面扔了,这里就先当做 defaultFailureUrl 不存在吧
        // 模拟打印异常信息
        response.setContentType("text/html;charset=UTF-8");
        response.sendError(HttpStatus.UNAUTHORIZED.value(),
                exception.getLocalizedMessage());
    //            super.onAuthenticationFailure(request, response, exception);
    }

注意: 之前我一直在说,异常了但是不会显示,不知道去哪里了。
这几次调试发现日志不断访问 /error ,好像这个在自己设置了BrowserSecurityConfig的拦截放行路径后,就一致这样了

阅读全文