Spring Security增加验证码校验


一、使用kaptcha生成验证码

kaptcha依赖包

<dependency>
    <groupId>com.github.pengglegroupId>
    <artifactId>kaptchaartifactId>
    <version>2.3.2version>
dependency>

kaptcha配置类

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer captcha(){
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "120");
        properties.setProperty("kaptcha.image.height", "45");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);

        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

ValidateCodeController中增加验证码图片的访问接口

@RestController
public class ValidateCodeController {

@Autowired
private Producer captchaProducer;

@GetMapping("/captcha.jpg")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
    response.setContentType("image/jpeg");
    String text = captchaProducer.createText();
    request.getSession().setAttribute("captcha", text);
    BufferedImage image = captchaProducer.createImage(text);
    ImageIO.write(image, "JPEG", response.getOutputStream());
}

二、增加验证码校验过滤器

Spring security的表单验证是通过过滤器链中的 UsernamePasswordAuthenticationFilter 来完成的,我们增加的验证码过滤器应该插在 UsernamePasswordAuthenticationFilter 之前,如果验证码校验不通过,直接返回,无需进行账户密码的校验。

Spring Security本身没有提供验证码校验的接口或者抽象类,需要开发人员自己去实现。下面实现一个 ValidateCodeFilter 来做验证码的过滤器,该过滤器将继承 OncePerRequestFilter,这个可以保证每次请求只调用一次该过滤器,因为我们调用一次就够了。

@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // authentication/form是认证时的请求接口,验证码校验只需要匹配这个接口即可
        if (StringUtils.equals("/authentication/form", request.getRequestURI()) &&
                StringUtils.equalsAnyIgnoreCase(request.getMethod(), "post")) {
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
            // 校验失败时,让失败的处理器去处理authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        // 无异常即校验成功,放行。
        filterChain.doFilter(request, response);

    }

    private void validate(ServletWebRequest request) throws ValidateCodeException {
        // 从session中获取验证码
        Object captcha = sessionStrategy.getAttribute(request, "captcha");
        // 从客户端接收到的验证码
        String captchaParam = request.getParameter("captcha");

        if (StringUtils.isEmpty(captchaParam)) {
            throw new ValidateCodeException("验证码不能为空");
        }

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

        if (!StringUtils.equalsAnyIgnoreCase(captcha.toString(), captchaParam)) {
            throw new ValidateCodeException("验证码不匹配");
        }
        // 校验成功之后,从session中移除验证码
        sessionStrategy.removeAttribute(request,"captcha");
    }
}

三、将过滤器插入到 UsernamePasswordAuthenticationFilter 之前

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
        .formLogin()
        .loginPage("myLogin.html")   // 登录页面
        .loginProcessingUrl("/authentication/form") //前端向后端发起认证的路径
        .successHandler(authenticationSuccessHandler)
        .failureHandler(authenticationFailureHandler)
        .and()
        .authorizeRequests()
        .antMatchers("myLogin.html", "/captcha.jpg").permitAll()   //当匹配到/authentication/require时,无需身份认证
        .anyRequest()
        .authenticated()
        .and()
        .csrf().disable();
}

前端页面:

<form action="/authentication/form" method="post">
    账户:<input type="text" name="username" /> <br>
    密码:<input type="text" name="password" /> <br>
    验证码:<input type="text" name="captcha"> <img src="/captcha.jpg" alt="#">
    <br>
    <input type="submit" value="登录">
form>