SpringBoot进阶教程(八十二)Spring Security图形验证码

打印 上一主题 下一主题

主题 838|帖子 838|积分 2514

在之前的博文《SpringBoot进阶教程(八十)Spring Security》中,已经介绍了在Spring Security中如何基于formLogin认证、基于HttpBasic认证和自定义用户名和暗码。这篇文章,我们将介绍自定义登录界面的登录验证方式。在上一篇博文《SpringBoot进阶教程(八十一)Spring Security自定义认证》中,已经介绍了如何实现Spring Security自定义认证。
v生成图形验证码

添加maven依赖
  1.         <dependency>
  2.             <groupId>org.springframework.social</groupId>
  3.             <artifactId>spring-social-config</artifactId>
  4.             <version>1.1.6.RELEASE</version>
  5.         </dependency>
复制代码
创建验证码对象
  1. /**
  2. * @Author chen bo
  3. * @Date 2023/12
  4. * @Des
  5. */
  6. @Data
  7. public class ImageCode {
  8.     /**
  9.      * image图片
  10.      */
  11.     private BufferedImage image;
  12.     /**
  13.      * 验证码
  14.      */
  15.     private String code;
  16.     /**
  17.      * 过期时间
  18.      */
  19.     private LocalDateTime expireTime;
  20.     public ImageCode(BufferedImage image, String code, int expireIn) {
  21.         this.image = image;
  22.         this.code = code;
  23.         this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
  24.     }
  25.     /**
  26.      * 判断验证码是否已过期
  27.      * @return
  28.      */
  29.     public boolean isExpire() {
  30.         return LocalDateTime.now().isAfter(expireTime);
  31.     }
  32. }
复制代码
创建ImageController 编写接口,返回图形验证码:
  1. /**
  2. * @Author chen bo
  3. * @Date 2023/12
  4. * @Des
  5. */
  6. @RestController
  7. public class ImageController {
  8.     public final static String SESSION_KEY_IMAGE_CODE = "SESSION_VERIFICATION_CODE";
  9.     private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
  10.     @GetMapping("https://www.cnblogs.com/code/image")
  11.     public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
  12.         ImageCode imageCode = createImageCode();
  13.         sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
  14.         ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
  15.     }
  16.     private ImageCode createImageCode() {
  17.         // 验证码图片宽度
  18.         int width = 100;
  19.         // 验证码图片长度
  20.         int height = 36;
  21.         // 验证码位数
  22.         int length = 4;
  23.         // 验证码有效时间 60s
  24.         int expireIn = 60;
  25.         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
  26.         Graphics graphics = image.getGraphics();
  27.         Random random = new Random();
  28.         graphics.setColor(getRandColor(200, 500));
  29.         graphics.fillRect(0, 0, width, height);
  30.         graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
  31.         graphics.setColor(getRandColor(160, 200));
  32.         for (int i = 0; i < 155; i++) {
  33.             int x = random.nextInt(width);
  34.             int y = random.nextInt(height);
  35.             int xl = random.nextInt(12);
  36.             int yl = random.nextInt(12);
  37.             graphics.drawLine(x, y, x + xl, y + yl);
  38.         }
  39.         StringBuilder sRand = new StringBuilder();
  40.         for (int i = 0; i < length; i++) {
  41.             String rand = String.valueOf(random.nextInt(10));
  42.             sRand.append(rand);
  43.             graphics.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
  44.             graphics.drawString(rand, 13 * i + 6, 16);
  45.         }
  46.         graphics.dispose();
  47.         return new ImageCode(image, sRand.toString(), expireIn);
  48.     }
  49.     private Color getRandColor(int fc, int bc) {
  50.         Random random = new Random();
  51.         if (fc > 255)
  52.             fc = 255;
  53.         if (bc > 255)
  54.             bc = 255;
  55.         int r = fc + random.nextInt(bc - fc);
  56.         int g = fc + random.nextInt(bc - fc);
  57.         int b = fc + random.nextInt(bc - fc);
  58.         return new Color(r, g, b);
  59.     }
  60. }
复制代码
org.springframework.social.connect.web.HttpSessionSessionStrategy对象封装了一些处置惩罚Session的方法,包罗了setAttribute、getAttribute和removeAttribute方法,具体可以查看该类的源码。使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。
v改造登录页面

添加验证码控件 在上一篇博文《SpringBoot进阶教程(八十一)Spring Security自定义认证》中的"重写form登录页",已经创建了login.html,在login.html中添加如下代码:
  1.         <span style="display: inline">
  2.             <input type="text" name="请输入验证码" placeholder="验证码" required="required"/>
  3.             <img src="https://www.cnblogs.com/code/image"/>
  4.         </span>
复制代码
img标签的src属性对应ImageController的createImageCode方法。
v认证流程添加验证码效验

定义异常类 在校验验证码的过程中,可能会抛出各种验证码范例的异常,比如“验证码错误”、“验证码已过期”等,所以我们定义一个验证码范例的异常类:
  1. /**
  2. * @Author chen bo
  3. * @Date 2023/12
  4. * @Des
  5. */
  6. public class ValidateCodeException extends AuthenticationException {
  7.     private static final long serialVersionUID = 1715361291615299823L;
  8.     public ValidateCodeException(String explanation) {
  9.         super(explanation);
  10.     }
  11. }
复制代码
注意:这里继承的是AuthenticationException而不是Exception。
创建验证码的校验过滤器 Spring Security实际上是由许多过滤器组成的过滤器链,处置惩罚用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter,而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后才去校验用户名和暗码。由于Spring Security并没有直接提供验证码校验相关的过滤器接口,所以我们需要自己定义一个验证码校验的过滤器ValidateCodeFilter:
  1. /**
  2. * @Author chen bo
  3. * @Date 2023/12
  4. * @Des
  5. */
  6. @Component
  7. public class ValidateCodeFilter extends OncePerRequestFilter {
  8.     @Autowired
  9.     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
  10.     private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
  11.     @Override
  12.     protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
  13.         if ("/login".equalsIgnoreCase(httpServletRequest.getRequestURI())
  14.                 && "post".equalsIgnoreCase(httpServletRequest.getMethod())) {
  15.             try {
  16.                 validateCode(new ServletWebRequest(httpServletRequest));
  17.             } catch (ValidateCodeException e) {
  18.                 myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
  19.                 return;
  20.             }
  21.         }
  22.         filterChain.doFilter(httpServletRequest, httpServletResponse);
  23.     }
  24.     private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {
  25.         ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ImageController.SESSION_KEY_IMAGE_CODE);
  26.         String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
  27.         if (StringUtils.isEmpty(codeInRequest)) {
  28.             throw new ValidateCodeException("验证码不能为空!");
  29.         }
  30.         if (codeInSession == null) {
  31.             throw new ValidateCodeException("验证码不存在!");
  32.         }
  33.         if (codeInSession.isExpire()) {
  34.             sessionStrategy.removeAttribute(servletWebRequest, ImageController.SESSION_KEY_IMAGE_CODE);
  35.             throw new ValidateCodeException("验证码已过期!");
  36.         }
  37.         if (!codeInRequest.equalsIgnoreCase(codeInSession.getCode())) {
  38.             throw new ValidateCodeException("验证码不正确!");
  39.         }
  40.         sessionStrategy.removeAttribute(servletWebRequest, ImageController.SESSION_KEY_IMAGE_CODE);
  41.     }
  42. }
复制代码
ValidateCodeFilter继承了org.springframework.web.filter.OncePerRequestFilter,该过滤器只会实验一次。ValidateCodeFilter继承了org.springframework.web.filter.OncePerRequestFilter,该过滤器只会实验一次。
在doFilterInternal方法中我们判定了请求URL是否为/login,该路径对应登录form表单的action路径,请求的方法是否为POST,是的话举行验证码校验逻辑,否则直接实验filterChain.doFilter让代码往下走。当在验证码校验的过程中捕获到异常时,调用Spring Security的校验失败处置惩罚器AuthenticationFailureHandler举行处置惩罚。
我们分别从Session中获取了ImageCode对象和请求参数imageCode(对应登录页面的验证码input框name属性),然后举行了各种判定并抛出相应的异常。当验证码过期或者验证码校验通过期,我们便可以删除Session中的ImageCode属性了。
v更新配置类

验证码校验过滤器定义好了,怎么才气将其添加到UsernamePasswordAuthenticationFilter前面呢?很简单,只需要在BrowserSecurityConfig的configure方法中添加些许配置即可,顺便配置验证码请求不配拦截: "https://www.cnblogs.com/code/image"。
  1. /**
  2. * @Author chen bo
  3. * @Date 2023/12
  4. * @Des
  5. */
  6. @Configuration
  7. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  8.     @Override
  9.     protected void configure(HttpSecurity http) throws Exception {
  10.         http.addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class) //添加验证码效验过滤器
  11.                 .formLogin() // 表单登录
  12.                 .loginPage("/login.html")       // 登录跳转url
  13. //                .loginPage("/authentication/require")
  14.                 .loginProcessingUrl("/login")   // 处理表单登录url
  15. //                .successHandler(authenticationSuccessHandler)
  16.                 .failureHandler(new MyAuthenticationFailureHandler())
  17.                 .and()
  18.                 .authorizeRequests()            // 授权配置
  19.                 .antMatchers("/login.html", "/css/**", "/authentication/require", "https://www.cnblogs.com/code/image").permitAll()  // 无需认证
  20.                 .anyRequest()                   // 所有请求
  21.                 .authenticated()                // 都需要认证
  22.                 .and().csrf().disable();
  23.     }
  24.     @Bean
  25.     public PasswordEncoder passwordEncoder() {
  26.         return new BCryptPasswordEncoder();
  27.     }
  28. }
复制代码
上面代码中,我们注入了ValidateCodeFilter,然后通过addFilterBefore方法将ValidateCodeFilter验证码校验过滤器添加到了UsernamePasswordAuthenticationFilter前面。
v运行效果图


其他参考/学习资料:
v源码地址

https://github.com/toutouge/javademosecond/tree/master/security-demo

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

灌篮少年

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表