Spring Security实战(二)—— 实现图形验证码

news/2024/12/2 22:28:41/

目录

一. 使用过滤器实现图形验证码

1. 自定义过滤器

2. 图形验证码过滤器

(1)引入kaptcha依赖

(2)配置一个 kaptcha 实例

(3)创建一个CaptchaController,用于获取图形验证码

(4)用于校验验证码的过滤器

(5)spring security 配置过滤器链

(6)修改 login.html 

(7)debug调试:

二、使用自定义认证实现图形验证码

1. 认识 AuthenticationProvider

2. 自定义AuthenticationProvider

 2. 实现图形验证码的AuthenticationProvider


一. 使用过滤器实现图形验证码

        验证码是为了防止恶意用户暴力重试而设置的。

1. 自定义过滤器

        在Spring Security中,实现验证码校验的方式有很多种,最简单的方式就是自定义一个专门处理验证码逻辑的过滤器,将其添加到Spring Security过滤器链的合适位置。当匹配到登录请求时,立刻对验证码进行校验,成功则放行,失败则提前结束整个验证请求。

(1)自定义一个过滤器

在该过滤器执行的时候在控制台输出一句日志

public class CustomFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {// 在这里添加自定义逻辑chain.doFilter(request, response);System.out.println("自定义的过滤器执行了!");}// 可以在这里实现 Filter 接口的其他方法
}

(2)把该过滤器加在UsernamePasswordAuthenticationFilter之后

    @Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class).authorizeRequests().antMatchers("/admin/api/**").hasRole("ADMIN").antMatchers("/user/api/**").hasRole("USER").anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable();}

(3)测试

登录前后,可以看到控制台会多次打印该过滤器内容

这可能是因为在 CustomFilter 的 doFilter() 方法中打印了一条日志,而该方法在请求到达服务器时就会被调用,而不是只有在登录时才执行。因此,无论是在登录前还是登录后,只要有请求到达服务器,CustomFilter 的 doFilter() 方法都会被调用,并输出一条日志。

2023-04-14 23:57:00.362  INFO 35992 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
自定义的过滤器执行了!
自定义的过滤器执行了!
2023-04-14 23:57:07.250  INFO 35992 --- [nio-8080-exec-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-04-14 23:57:07.410  INFO 35992 --- [nio-8080-exec-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
自定义的过滤器执行了!
自定义的过滤器执行了!

2. 图形验证码过滤器

        想要实现图形验证码校验功能,首先应当有一个获取图形验证码的API,绘制图形验证码的方法有很多,使用开源的验证码组件即可。例如kaptcha

(1)引入kaptcha依赖

        <dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version></dependency>

(2)配置一个 kaptcha 实例

    @Beanpublic Producer captcha() {Properties properties = new Properties();properties.setProperty("kaptcha.image.width","150");properties.setProperty("kaptcha.image.hright","150");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;}

这个代码片段是一个Spring的Java配置方法。它使用Kaptcha库创建一个实现验证码服务的Java Bean。下面是代码块的各部分解释:

  1. @Bean - 这是Spring注解,用于告诉Spring容器,要将此方法返回的对象作为bean注册到容器中。

  2. Properties - 这是一个Java类,用于管理一组键值对。这里我们使用Properties来存储Kaptcha配置属性。

  3. Config - 这是Kaptcha库中的一个Java类,它需要从上述Properties对象中加载一组Kaptcha配置属性。

  4. DefaultKaptcha - 这是Kaptcha库中的一个Java类,它实现了默认的验证码生成算法。

  5. setConfig - 这是DefaultKaptcha类中的一个setter方法,用于将从Config对象中读取的属性设置到DefaultKaptcha实例中。

  6. return defaultKaptcha - 最后,这个方法返回一个DefaultKaptcha实例,它包含了我们所需的所有配置信息,可以生成图形验证码。

(3)创建一个CaptchaController,用于获取图形验证码

@Controller
public class CaptchaController {@Autowiredprotected Producer captchaProducer;@GetMapping("/captcha.jpg")public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {//设置内容类型response.setContentType("image/jpeg");//创建验证码文本String capText = captchaProducer.createText();//将验证码文本设置到 sessionrequest.getSession().setAttribute("captcha",capText);//创建验证码图片BufferedImage bi = captchaProducer.createImage(capText);//获取响应输出流ServletOutputStream out = response.getOutputStream();//将图形验证码数据写道响应输出流ImageIO.write(bi,"jpg",out);try {out.flush();} finally {out.close();}}
}
  •  @GetMapping("/captcha.jpg") - 这是Spring MVC注解,用于将控制器方法映射到GET请求"/captcha.jpg"。
  • response.setContentType("image/jpeg"); - 这一行设置响应的内容类型为JPEG图像。
  • String capText = captchaProducer.createText(); - 这一行使用captchaProducer对象创建一个包含随机验证码文本的字符串capText。
  • request.getSession().setAttribute("captcha",capText); - 这一行将capText保存到当前HTTP会话的属性“captcha”中,以便稍后验证输入。
  • BufferedImage bi = captchaProducer.createImage(capText); - 这一行使用captchaProducer对象创建一个包含capText文本的验证码图片bi。
  • ServletOutputStream out = response.getOutputStream(); - 这一行获取HTTP响应输出流对象out,以便我们可以将验证码图像数据写入响应流。
  • ImageIO.write(bi,"jpg",out); - 这一行将bi对象的图像数据写入out输出流中。
  • out.flush(); - 这一行刷新输出流。
  • out.close(); - 最后,使用out.close()关闭输出流。

        当用户访问 /captcha.jpg时,即可得到一张携带验证码的图片,验证码文本则被存放到session中,用于后续校验。现在我们可以启动项目先看一下:

(4)用于校验验证码的过滤器

        虽然Spring Security 的过滤器链对过滤器没有特殊要求,只要继承了Filter即可,但是在Spring体系中,推荐使用OncePerRequestFilter来实现。它可以确保一次请求只会通过一次该过滤器(Filter实际上并不能保证这一点)

public class CaptchaFilter extends OncePerRequestFilter {private static final String CAPTCHA_SESSION_KEY = "captcha";private static final String CAPTCHA_PARAM_NAME = "captcha";@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {if (request.getMethod().equalsIgnoreCase("POST")) { //只对POST请求进行验证码验证HttpSession session = request.getSession(false);if (session != null) {//拿到session中存放的 captcha 属性String captcha = (String) session.getAttribute(CAPTCHA_SESSION_KEY);if (captcha == null) {response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "验证码已过期,请重新获取。");return;}//获取输入的验证码信息String inputCaptcha = request.getParameter(CAPTCHA_PARAM_NAME);if (inputCaptcha == null || !captcha.equals(inputCaptcha.trim())) {response.sendError(HttpServletResponse.SC_BAD_REQUEST, "验证码错误,请重新输入。");return;}} else {response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "无法验证验证码,因为HTTP会话不存在。");return;}}filterChain.doFilter(request, response);}
}

 这是一个验证码过滤器类,用于在用户登录等需要输入验证码的场景下,对用户输入的验证码进行验证。具体来说:

  • 在doFilterInternal()方法中,首先通过request对象获取用户请求的方法,如果是POST请求,则对验证码进行验证。
  • 然后通过request.getSession(false)获取当前的session对象,如果session对象不为null,则继续进行验证码验证;否则返回错误信息,提示无法验证验证码,因为HTTP会话不存在。
  • 验证码的具体验证流程是:先通过CAPTCHA_SESSION_KEY获取session中存放的验证码信息,如果获取到的验证码为null,则返回错误信息,提示验证码已过期,请重新获取;否则获取用户输入的验证码信息,如果用户输入的验证码为null或者与session中的验证码信息不一致,则返回错误信息,提示验证码错误,请重新输入。
  • 最后,如果验证码验证通过,则通过filterChain.doFilter()方法继续执行后续的过滤器或请求处理。 总之,这个验证码过滤器类的作用是保证用户输入的验证码正确,从而增强了系统的安全性和防范机制。

(5)spring security 配置过滤器链

    @Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(new CaptchaFilter(),UsernamePasswordAuthenticationFilter.class).authorizeRequests().antMatchers("/admin/api/**").hasRole("ADMIN").antMatchers("/user/api/**").hasRole("USER")//开放验证码的访问权限.antMatchers("/captcha.jpg").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable();}

(6)修改 login.html 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Login Page</title><style>/* 样式可以自行修改 */body {background-color: cadetblue;}.login-form {width: 350px;margin: 150px auto;background-color: #fff;padding: 20px;box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);}h1 {font-size: 24px;text-align: center;margin-bottom: 30px;}input[type="text"], input[type="password"], input[type="number"] {width: 100%;padding: 10px;margin-bottom: 20px;border: 2px solid #ccc;border-radius: 4px;box-sizing: border-box;}button {background-color: darksalmon;color: white;padding: 12px 20px;border: none;border-radius: 4px;cursor: pointer;width: 100%;}button:hover {background-color: #45a049;}.captcha-container {text-align: center;margin-bottom: 20px;}.captcha-img {width: 150px;height: 50px;}</style>
</head>
<body>
<div class="login-form"><h1>Login Page</h1><form th:action="@{/login}" method="post"><label for="username">Username</label><input type="text" id="username" name="username" placeholder="Enter username" required><label for="password">Password</label><input type="password" id="password" name="password" placeholder="Enter password" required><div class="captcha-container"><img class="captcha-img" th:src="@{/captcha.jpg}" onclick="this.src='captcha.jpg?'+Math.random()"></div><label for="captcha">Verification Code</label><input type="number" id="captcha" name="captcha" placeholder="Enter verification code" required><button type="submit">Login</button></form>
</div>
</body>
</html>

(7)debug调试:

 

二、使用自定义认证实现图形验证码

        上面使用过滤器方式实现类带图形验证码的验证功能,属于Servlet层面,Spring Security还提供了一种更优雅的实现图形验证码的方式,即自定义认证。

1. 认识 AuthenticationProvider

        我们所面对的系统中的用户,在Spring Security中被称为主体(principal),主体包含了所有能够经过验证而获得系统访问权限的用户、设备或其他系统。Spring Security 通过一层将其定义为一个Authentication。

public interface Authentication extends Principal, Serializable {//获取主体权限列表Collection<? extends GrantedAuthority> getAuthorities();//获取主体凭据,通常为用户密码Object getCredentials();//获取主体详细信息Object getDetails();//获取主体,通常为一个用户名Object getPrincipal();//主体是否验证成功boolean isAuthenticated();void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

        UsernamePasswordAuthenticationToken也是Authentication的一个实现类。         

        大部分情况下身份验证都是基于用户名和密码进行的,所以 Spring Security提供了一个UsernamePasswordAuthenticationToken 用于代指这一类证明,例如用SSH KEY也可以登录,但它不属于用户名和密码登录这个范畴。

        在前面使用的表单登录中,每一个登录用户被包装为一个UsernamePasswordAuthenticationToken,从而在Spring Security的各个AuthenticationProvider中流动。

        AuthenticationProvider 被Spring Security定义为一个验证过程,一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。

public interface AuthenticationProvider {
//验证过程,成功的话返回一个验证完成的AuthenticationAuthentication authenticate(Authentication authentication) throws AuthenticationException;boolean supports(Class<?> authentication);
}

2. 自定义AuthenticationProvider

        Spring Security提供了多种常见的认证技术,包括但不限于以下几种:

  • HTTP层面的认证技术,包括HTTP基本认证和HTTP摘要认证两种
  • 基于LDAP的认证技术
  • 聚焦于证明用户身份的OpenID认证技术
  • 聚焦于授权的OAuth认证技术
  • 系统内维护的用户名和密码认证技术(最广泛)

         系统内维护的用户名和密码认证技术使用最为广泛,通常会涉及数据库访问。为了更好的按需定制,Spring Security并没有直接糅合整个认证过程,而是提供了一个抽象的AuthenticationProvider,那就是 AbstractUserDetailsAuthenticationProvider

 

AbstractUserDetailsAuthenticationProvider是Spring Security提供的抽象类,用于支持验证用户身份,并从用户详细信息中构建身份验证对象

该类实现了AuthenticationProvider接口,可以作为身份验证的提供者。同时,它还实现了UserDetailsService接口,用于从数据源中获取用户详细信息。

在具体的实现中,AbstractUserDetailsAuthenticationProvider的主要作用是将Authentication对象中的用户名和密码提取出来,然后通过UserDetailsService获取用户详细信息,并将其与用户名和密码进行比较,以确定用户的身份是否正确。

如果身份验证成功,AbstractUserDetailsAuthenticationProvider会将用户详细信息封装到一个新的身份验证对象中,并将其返回。如果身份验证失败,则会抛出一个AuthenticationException异常。

此外,AbstractUserDetailsAuthenticationProvider还可以处理密码加密和解密的逻辑,以及支持定制化的身份验证逻辑。

实现:自定义AuthenticationProvider示例

public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {@Autowiredprivate CustomUserDetailsService userDetailsService;@Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {// 在此处实现密码验证逻辑if (!authentication.getCredentials().equals(userDetails.getPassword())) {throw new BadCredentialsException("Invalid username or password");}}@Overrideprotected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {// 在此处从自定义的用户详情服务中获取用户信息UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (userDetails == null) {throw new UsernameNotFoundException("User not found with username: " + username);}return userDetails;}}

 2. 实现图形验证码的AuthenticationProvider

        现在重新回到自定义认证实现图形验证码登录的这个案例中,由于只是在常规的认证之上增加了图形验证码的校验,其他流程并没有变化,所以只需要继承DaoAuthenticationProvider并稍微修改即可。

public class MyAuthenticationProvider extends DaoAuthenticationProvider {public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {this.setUserDetailsService(userDetailsService);this.setPasswordEncoder(passwordEncoder);}@Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {//实现图形验证码的校验逻辑//调用父类方法完成密码验证super.additionalAuthenticationChecks(userDetails, authentication);}
}

        用户提交的验证码和session存储的验证码都需要从用户的请求中获取,但是传入的对象只有userDetails 和 authentication。是否还需要一个HttpServletRequest对象呢?并非如此,Authentication实际上还可以携带账号信息之外的数据。

public interface Authentication extends Principal, Serializable {//允许携带任意对象 Object类型Object getDetails();}

        前面提到过,一次完整的认证可以包含多个AuthenticationProvider,这些AuthenticationProvider都由ProviderManager管理,ProviderManager是由UsernamePasswordAuthenticationFilter调用的。

AuthenticationProviderProviderManager介绍:

        AuthenticationProvider和ProviderManager是Spring Security框架中用来实现认证的两个关键组件。

        AuthenticationProvider是一个接口,它定义了认证的核心逻辑,即对用户提供的认证信息进行验证。其中包含一个方法authenticate(),用于执行认证操作。在Spring Security中,一般情况下需要自定义实现AuthenticationProvider接口,并将其添加到ProviderManager中,以完成对认证请求的处理。

        ProviderManager是一个认证管理器,它负责协调多个AuthenticationProvider实现,实现对认证请求的分派、调度和结果处理。当认证请求到达ProviderManager时,它会遍历其内部持有的AuthenticationProvider实现列表,逐一尝试调用各个AuthenticationProvider的authenticate()方法,直到其中一个AuthenticationProvider能够成功认证该请求,或者所有AuthenticationProvider都无法认证成功。

        总体来说,AuthenticationProvider和ProviderManager是Spring Security框架中非常重要的两个组件,它们共同协作,实现了对用户身份的验证和认证。通过实现AuthenticationProvider接口,可以对认证逻辑进行个性化定制,而通过使用ProviderManager,可以方便地协调多个AuthenticationProvider实现,以实现更加复杂的认证方案。

AuthenticationProviderProviderManagerUsernamePasswordAuthenticationFilter的关系:

        AuthenticationProvider和ProviderManager都是与身份验证相关的组件,而UsernamePasswordAuthenticationFilter是处理身份验证请求的过滤器。

        当用户提交身份验证请求时,UsernamePasswordAuthenticationFilter拦截请求并从请求中获取用户名和密码等身份验证信息,然后创建一个UsernamePasswordAuthenticationToken对象来表示这些信息,并将其传递给ProviderManager的authenticate()方法进行身份验证。

         ProviderManager通过遍历已配置的AuthenticationProvider列表来查找可以处理该UsernamePasswordAuthenticationToken的AuthenticationProvider。如果找到了匹配的AuthenticationProvider,则该提供程序会对身份验证信息进行验证,并返回一个经过身份验证的Authentication对象,ProviderManager将该Authentication对象传递给UsernamePasswordAuthenticationFilter,以表明身份验证已成功。

        如果ProviderManager无法找到匹配的AuthenticationProvider,或者已找到但是无法验证身份验证信息,则ProviderManager将抛出一个AuthenticationException异常,UsernamePasswordAuthenticationFilter将捕获该异常并处理身份验证失败的情况。

如图:

 

         Authentication中有了HttpServletRequest之后,一切变得非常顺畅,基于图形验证码的场景,我们可以继承WebAuthenticationDetails,并扩展需要的信息。

public class MyWebAuthenticationDetails extends WebAuthenticationDetails {private boolean imageCodeIsRight;public boolean getImageCodeIsRight() {return this.imageCodeIsRight;}//补充用户提交的验证码和session保存的验证码public MyWebAuthenticationDetails(HttpServletRequest request) {super(request);String imageCode = request.getParameter("captcha");HttpSession session = request.getSession();String savedImageCode = (String)session.getAttribute("captcha");if (!StringUtils.isEmpty(savedImageCode)) {session.removeAttribute("captcha");//当验证码正确时设置状态if (!StringUtils.isEmpty(imageCode) && imageCode.equals(savedImageCode)) {this.imageCodeIsRight = true;}}}
}

将它提供给一个自定义的AuthenticationDetailsSource

public class MyWebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {@Overridepublic WebAuthenticationDetails buildDetails(HttpServletRequest context) {return new MyWebAuthenticationDetails(context);}
}

 接下来实现自定义的AuthenticationProvider

public class MyAuthenticationProvider extends DaoAuthenticationProvider {public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {this.setUserDetailsService(userDetailsService);this.setPasswordEncoder(passwordEncoder);}@SneakyThrows@Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {//实现图形验证码的校验逻辑//获取详细信息MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();//一旦发现验证码不正确,就立刻抛出异常信息if (!details.getImageCodeIsRight()) {throw new VerificationCodeException();}//调用父类方法完成密码验证super.additionalAuthenticationChecks(userDetails, authentication);}
}


http://www.ppmy.cn/news/45085.html

相关文章

92-TCP三次握手及TCP四次挥手

TCP三次握手及TCP四次挥手1.tcp三次握手(1)tcp的特点(2)tcp三次握手发生在什么阶段(3)tcp协议报头(4)tcp三次握手的流程2.tcp四次挥手(1)tcp四次挥手发生在什么阶段(2)tcp四次挥手的流程(3)能不能将服务器发端发送的ACK和FIN放在一起发送呢1.tcp三次握手 (1)tcp的特点 TCP 协…

恢复照片软件推荐,照片恢复就这么做!

案例&#xff1a;好用的恢复照片软件 【作为一名摄影博主&#xff0c;我每天拍的照片太多了&#xff0c;在筛选的时候总是容易错删重要的照片&#xff0c;大家有什么比较好的照片恢复软件或方法可以推荐吗&#xff1f;万分期待!】 随着数字化时代的发展&#xff0c;人们越来越…

【数据库基操】启动与连接MySQL数据库

一、启动与关闭 只介绍一种方法&#xff1a; 打开命令行工具&#xff0c;以管理员身份运行 1.启动数据库 net start mysql80 //80是在安装的时候设置的名字&#xff08;默认&#xff09;&#xff0c;不用在意 2.关闭数据库 net stop mysql80 如题已经成功&#…

决策树相关知识点

为什么id3和c4.5采用多叉树而cart采用二叉树&#xff1f; ID3 和 C4.5 采用的多叉树虽然在对训练样本集的学习中可以尽可能多地挖掘信息&#xff0c;但是其生成的决策树分支、规模都比较大&#xff0c;训练特别慢&#xff0c;CART 算法的二分法可以简化决策树的规模&#xff0…

Python不可变对象与可变对象

文章目录对象类型不可变对象&#xff08;值类型&#xff09;可变对象&#xff08;引用类型&#xff09;不可变对象的特例Python变量不可变对象&#xff08;值类型&#xff09;可变对象&#xff08;引用类型&#xff09;参数传递Python语言是一个以一切皆对象的面向对象的动态型…

STM32F4_定时器精讲(TIM)

目录 1. 什么是定时器&#xff1f; 2. STM32定时器简介 2.1 高级控制定时器 TIM1和TIM8 2.1.1 TIM1和TIM8简介 2.1.2 时基单元 2.1.3 计数器模式 2.1.4 重复计数器 2.1.5 时钟选择 2.1.6 捕获/比较通道 2.1.7 输入捕获模式 2.1.8 其他功能 2.2 通用定时器 TIM2到TI…

webgl-简单动画

html <!DOCTYPE html> <head> <style> *{ margin: 0px; padding: 0px; } </style> </head> <body> <canvas id webgl> 您的浏览器不支持HTML5,请更换浏览器 </canvas> <script src"./main.js"></script&g…

时隔两个多月,一起来看ChatGPT现况如何?

ChatGPT这股风吹了两个多月&#xff0c;时至今日&#xff0c;各平台上与ChatGPT相关的文章&#xff0c;到现在依旧拥有着不小的流量。三月中旬上线了ChatGPT-4&#xff0c;与我们的文心一言前后脚发布&#xff0c;而后阿里的“通义千问”也展现了不俗的实力&#xff0c;那到现在…