目录
一. 使用过滤器实现图形验证码
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。下面是代码块的各部分解释:
@Bean
- 这是Spring注解,用于告诉Spring容器,要将此方法返回的对象作为bean注册到容器中。
Properties
- 这是一个Java类,用于管理一组键值对。这里我们使用Properties
来存储Kaptcha配置属性。
Config
- 这是Kaptcha库中的一个Java类,它需要从上述Properties
对象中加载一组Kaptcha配置属性。
DefaultKaptcha
- 这是Kaptcha库中的一个Java类,它实现了默认的验证码生成算法。
setConfig
- 这是DefaultKaptcha
类中的一个setter方法,用于将从Config
对象中读取的属性设置到DefaultKaptcha
实例中。
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调用的。
AuthenticationProvider和ProviderManager介绍:
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实现,以实现更加复杂的认证方案。
AuthenticationProvider和ProviderManager与UsernamePasswordAuthenticationFilter的关系:
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);}
}