Spring Boot整合Security系列步骤及问题排查(五)—— 自定义图形验证码校验

1.新建图形验证码实体类:

/**
 * 图片验证码
 *
 * @author zhaohaibin
 */
@Data
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 ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }

    /**
     * 是否过期验证
     *
     * @return
     */
    public boolean isExpried() {
        return LocalDateTime.now().isAfter(expireTime);
    }
}

2.新建图形验证码控制器及生成逻辑:

/**
 * 验证码控制器
 *
 * @author zhaohaibin
 */
@RestController
public class ValidateCodeController {

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    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);
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    /**
     * 生成图形验证码
     *
     * @param request
     * @return
     */
    private ImageCode createImageCode(HttpServletRequest request) {

        int width = 67;
        int height = 23;

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        return new ImageCode(image, sRand, 60);

    }

    /**
     * 生成随机背景条纹
     *
     * @param fc
     * @param bc
     * @return
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

}

3.自定义验证码拦截器:

/**
 * 自定义验证码拦截器
 * @author zhaohaibin
 */
@Data
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {

    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // /demo:项目名前缀
        if (StringUtils.equals("/demo/authentication/form", request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {

            try {

                validate(new ServletWebRequest(request));

            } catch (ValidateCodeException e) {

                response.setContentType("application/json;charset=UTF-8");
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);

                return;

            }

        }

        // 否则执行后续过滤
        filterChain.doFilter(request, response);

    }

    /**
     * 验证验证码
     *
     * @param request
     * @throws ServletRequestBindingException
     * @throws ValidateCodeException
     */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {

        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);

        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }

        if (null == codeInSession) {
            throw new ValidateCodeException("验证码不存在");
        }

        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }

        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }

        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);

    }
}

ValidateCodeException:

/**
 * 验证码自定义异常类
 *
 * @author zhaohaibin
 */
public class ValidateCodeException extends AuthenticationException {

    private static final long serialVersionUID = -6740942343625021592L;

    public ValidateCodeException(String msg) {
        super(msg);
    }

}

4.更新WebSecurityConfig:

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(demoAuthenticationFailureHandler);

        // 默认/表单登录方式
//        http.httpBasic()
        // 在默认UsernamePasswordAuthenticationFilter前增加自定义验证码校验拦截,并传入自定义失败对象
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                // 自定义登录页面
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(demoAuthenticationSuccessHandler)
                .failureHandler(demoAuthenticationFailureHandler)
                .and()
                // 对任何请求授权
                .authorizeRequests()
                // 匹配页面授权所有权限
                .antMatchers(
                        // API
                        "/swagger-ui.html",
                        // 默认登录页
                        "/authentication/require",
                        // 自定义登录页(demoLogin)
                        securityProperties.getBrowser().getLoginPage(),
                        "/code/image").permitAll()
                // 任何请求
                .anyRequest()
                // 都需要被认证
                .authenticated()
                .and()
                // 请求伪造防护功能关闭
                .csrf().disable();

    }

5.更新登录页面:

    <div><label>验证码</label><input type="text" name="imageCode" placeholder="请输入验证码"/><img src="code/image" alt=""></div>

问题排查:

HttpSessionSessionStrategy对应maven引入:

<!-- https://mvnrepository.com/artifact/org.springframework.social/spring-social-web -->
        <dependency>
            <groupId>org.springframework.social</groupId>
            <artifactId>spring-social-web</artifactId>
            <version>1.1.6.RELEASE</version>
        </dependency>

总是跳转"引导至登录页",注释登录类型配置:

# security 默认登录页面配置
demo:
  security:
    browser:
      loginPage: "/demoLogin.html"
#      loginType: "REDIRECT"
发布了81 篇原创文章 · 获赞 12 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/u012382791/article/details/105263043