Spring Security
添加图形验证码
引入所需maven依赖
<dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>1.1.4.RELEASE</version> </dependency>
添加验证码流程
-
后端根据随机数生成验证码图片
-
前端请求登录页的同时向后端请求验证码图片
-
在
Spring Security
的认证流程中添加验证码的校验
验证码的校验应该在用户名和密码之前,而spring security
的用户名和密码校验是在UsernamePasswordAuthenticationFilter
过滤器中完成的,所以我们的验证码校验过滤器应该在它之前执行。
首先先定义一个验证码图片对象
public class ImageCode { //验证码图片 private BufferedImage image; //验证码 private String code; //过期时间 private LocalDateTime expireTime; 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; } //判断验证码是否过期 public boolean isExpire(){ return LocalDateTime.now().isAfter(expireTime); } //getter 和 setter方法 }
然后编写一个ValidateCodeController类用于处理生成验证码图片的请求
@RestController @RequestMapping("code") public class ValidateController { public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE"; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @GetMapping("img") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { ImageCode imageCode = createImageCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode); //以流的形式写到前端页面 ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream()); } //生成验证码图片 private ImageCode createImageCode() { int width = 100; // 验证码图片宽度 int height = 36; // 验证码图片长度 int length = 4; // 验证码位数 int expireIn = 60; // 验证码有效时间 60s 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); } StringBuilder sRand = new StringBuilder(); for (int i = 0; i < length; i++) { String rand = String.valueOf(random.nextInt(10)); sRand.append(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.toString(), expireIn); } 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); } }
改造登录页,把下面html代码写入form表单中,src="/code/img"
对应的是ValidateCodeController
的createImageCode
方法的请求路径
<input type="text" name="imageCode" placeholder="验证码" style="width: 50%;"/> <img src="/code/img"/>
修改spring security
配置类,让生成验证码的请求无需认证
@Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginPage("/login")//配置登录页 .loginProcessingUrl("/login") .successHandler(myAuthenticationSucessHandler) .failureHandler(myAuthenticationFailureHandler) .permitAll()//可不认证访问 .and() .authorizeRequests()// 授权配置 .antMatchers("/test/error","/code/img").permitAll()//无需认证 .anyRequest() // 所有请求 .authenticated()// 都需要认证 ; }
在校验验证码的过程会抛出各种异常,如“验证码错误”、“验证码为空”、“验证码过期”等异常,我们可以写一个验证码类型的异常
public class ValidateCodeException extends AuthenticationException { private static final long serialVersionUID = 2624478717043756543L; public ValidateCodeException(String msg) { super(msg); } }
接着写校验验证码的过滤器
@Component public class ValidateCodeFilter extends OncePerRequestFilter { @Autowired private AuthenticationFailureHandler authenticationFailureHandler; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { System.out.println(request.getRequestURI()); System.out.println(request.getMethod()); //请求为 /login,并且请求方法为 post if (StringUtils.equalsIgnoreCase("/login", request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(), "post")){ try{ validateCode(new ServletWebRequest(request)); }catch (ValidateCodeException e) { authenticationFailureHandler.onAuthenticationFailure(request, response, e); return; } } //放行 filterChain.doFilter(request, response); } private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); String imageInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode"); if (StringUtils.isEmpty(imageInRequest)){ throw new ValidateCodeException("验证码不能为空"); } if (codeInSession == null) { throw new ValidateCodeException("验证码不存在"); } if (codeInSession.isExpire()) { throw new ValidateCodeException("验证码已过期"); } if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(),imageInRequest)) { throw new ValidateCodeException("验证码不正确"); } sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); } }
验证码校验的过滤器继承了OncePerRequestFilter
,该过滤器每个请求只会执行一次
最后要将我们自定义的过滤器添加到用户名和密码校验的过滤器之前,这里需要修改spring security
配置类,在此过滤器添加到UsernamePasswordAuthenticationFilter
之前
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationSucessHandler myAuthenticationSucessHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private ValidateCodeFilter validateCodeFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/login")//配置登录页 .loginProcessingUrl("/login") .successHandler(myAuthenticationSucessHandler) .failureHandler(myAuthenticationFailureHandler) .permitAll()//可不认证访问 .and() .authorizeRequests()// 授权配置 .antMatchers("/test/error","/code/img").permitAll() .anyRequest() // 所有请求 .authenticated()// 都需要认证 ; } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/static/**"); } }
到此,spring security
添加图形验证码就完成