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>