# 深入SpringSecurity ### 环境介绍 - `SpringBoot`: `2.7.10` - `spring-boot-starter-security`: `2.7.10` - `SpringSecurity`: `5.7.7` ### 导入依赖 ```xml org.springframework.boot spring-boot-starter-security ``` ### 基于表单认证 **默认使用表单进行验证**
{{< image src="/images/all/image-20230413144242844.png" caption="">}}
密码由`SpringSecurity`自动生成,用户名为`user`.
{{< image src="/images/all/image-20230413144652041.png" caption="">}}
### 基于HTTP basic认证 也可以换成`HTTP basic`类型的认证 加入使用下面的配置 ```java import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() // httpBasic方式 .and() .authorizeRequests() // 授权配置 .anyRequest() // 所有请求 .authenticated(); // 都需要认证 } } ``` Spring Security提供了这种链式的方法调用。上面配置指定了认证方式为`HTTP basic`登录,并且所有请求都需要进行认证。 重启项目,可看到页面弹出了个`HTTP Basic`认证框
{{< image src="/images/all/image-20230413145039147.png" caption="">}}
如果要切换回表单验证,可以直接将配置类删掉,或者修改为表单验证方式 ```java import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单方式 .and() .authorizeRequests() // 授权配置 .anyRequest() // 所有请求 .authenticated(); // 都需要认证 } } ``` ### 自定义认证过程 自定义认证的过程需要实现`Spring Security`提供的`UserDetailsService`接口 ```java public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } ``` `loadUserByUsername`方法返回一个`UserDetail`对象,该对象也是一个接口,包含一些用于描述用户信息的方法 ``` public interface UserDetails extends Serializable { Collection getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); } ``` 这些方法的含义如下: - `getAuthorities`获取用户包含的权限,返回权限集合,权限是一个继承了`GrantedAuthority`的对象; - `getPassword`和`getUsername`用于获取密码和用户名; - `isAccountNonExpired`方法返回`boolean`类型,用于判断账户是否未过期,未过期返回`true`反之返回`false`; - `isAccountNonLocked`方法用于判断账户是否未锁定; - `isCredentialsNonExpired`用于判断用户凭证是否没过期,即密码是否未过期; - `isEnabled`方法用于判断用户是否可用。 > 实际中我们可以自定义`UserDetails`接口的实现类,也可以直接使用`Spring Security`提供的`UserDetails`接口实现类`org.springframework.security.core.userdetails.User` 创建一个实体类 ```java @Data public class MyUser implements Serializable { private static final long serialVersionUID = 7225349028293972637L; private String userName; private String password; // 用户未过期 private boolean accountNonExpired = true; // 用户未锁定 private boolean accountNonLocked= true; // 权限认证未过期 private boolean credentialsNonExpired= true; // 用户可用 private boolean enabled= true; } ``` 接着创建`MyUserDetailService`实现`UserDetailService`: ```java @Configuration public class UserDetailService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 模拟一个用户,替代数据库获取逻辑 MyUser user = new MyUser(); user.setUserName(username); user.setPassword(this.passwordEncoder.encode("123456")); // 输出加密后的密码 System.out.println(user.getPassword()); return new User(username, user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } } ``` 上面使用了`PasswordEncoder`对明文密码加密,我们需要在配置类中进行注册。 ```java @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { /** * 明文密码加密 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } ... } ``` 这里我们使用了`org.springframework.security.core.userdetails.User`类包含7个参数的构造器,其还包含一个三个参数的构造器`User(String username, String password,Collection authorities)`,由于权限参数不能为空,所以这里先使用`AuthorityUtils.commaSeparatedStringToAuthorityList`方法模拟一个admin的权限,该方法可以将逗号分隔的字符串转换为权限集合。 重启项目,访问[http://localhost:8080/login](http://localhost:8080/login),便可以使用任意用户名以及123456作为密码登录系统。可以看到控制台输出的加密后的密码(每次执行生成的密码不一样)
{{< image src="/images/all/image-20230413151835439.png" caption="">}}
### 自定义登录页 在`resources/static/`下新建一个`login.html`: ```html 登录

账户登录



``` `login.css`如下: ```css .login-page { width: 360px; padding: 8% 0 0; margin: auto; } .form { position: relative; z-index: 1; background: #ffffff; max-width: 360px; margin: 0 auto 100px; padding: 45px; text-align: center; box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); } .form input { outline: 0; background: #f2f2f2; width: 100%; border: 0; margin: 0 0 15px; padding: 15px; box-sizing: border-box; font-size: 14px; } .form button { text-transform: uppercase; outline: 0; background: #4caf50; width: 100%; border: 0; padding: 15px; color: #ffffff; font-size: 14px; -webkit-transition: all 0.3 ease; transition: all 0.3 ease; cursor: pointer; } .form button:hover, .form button:active, .form button:focus { background: #43a047; } .form .message { margin: 15px 0 0; color: #b3b3b3; font-size: 12px; } .form .message a { color: #4caf50; text-decoration: none; } .form .register-form { display: none; } .container { position: relative; z-index: 1; max-width: 300px; margin: 0 auto; } .container:before, .container:after { content: ""; display: block; clear: both; } .container .info { margin: 50px auto; text-align: center; } .container .info h1 { margin: 0 0 15px; padding: 0; font-size: 36px; font-weight: 300; color: #1a1a1a; } .container .info span { color: #4d4d4d; font-size: 12px; } .container .info span a { color: #000000; text-decoration: none; } .container .info span .fa { color: #ef3b3a; } body { background: #76b852; /* fallback for old browsers */ background: -webkit-linear-gradient(right, #76b852, #8dc26f); background: -moz-linear-gradient(right, #76b852, #8dc26f); background: -o-linear-gradient(right, #76b852, #8dc26f); background: linear-gradient(to left, #76b852, #8dc26f); font-family: Lato,"PingFang SC","Microsoft YaHei",sans-serif; } ``` 修改`BrowserSecurityConfig`中的`configure`方法 ```java Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 .loginPage("/login.html") // 登录页 .loginProcessingUrl("/login") // 处理表单登录URL .and() .authorizeRequests() // 授权配置 .antMatchers("/login.html","/css/*").permitAll() // 未登录时允许访问login.html .anyRequest() // 所有请求 .authenticated() // 除放行的链接和资源其他都需要认证 .and() .csrf().disable(); // 关闭csrf } ``` 上面代码中`.loginPage("/login.html")`指定了跳转到登录页面的请求URL,`.loginProcessingUrl("/login")`对应登录页面form表单的`action="/login"`,`.antMatchers("/login.html").permitAll()`表示跳转到登录页面的请求不被拦截,否则会进入无限循环。 这时候启动系统,访问[http://localhost:8080/hello](http://localhost:8080/hello),会看到页面已经被重定向到了[http://localhost:8080/login.html](http://localhost:8080/login.html)
{{< image src="/images/all/image-20230413204212199.png" caption="">}}
**假如有这样一个需求:在未登录的情况下,当用户访问`html`资源的时候跳转到登录页,未登录下其他访问请求则返回"您访问的资源需要身份认证!",状态码为401。** 要实现这个功能我们将`loginPage`的URL改为`/authentication/require`,并且在`antMatchers`方法中加入该URL,让其免拦截。 1.修改`BrowserSecurityConfig`中的`configure`方法 ```java Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 .loginPage("/authentication/require") // 登录跳转 URL .loginProcessingUrl("/login") // 处理表单登录URL .and() .authorizeRequests() // 授权配置 .antMatchers("/authentication/require","/login.html").permitAll() // 未登录时允许访问login.html .anyRequest() // 所有请求 .authenticated() // 除放行的链接和资源其他都需要认证 .and() .csrf().disable(); // 关闭csrf } ``` 2.新增一个`BrowserSecurityController`用来处理请求 ```java @RestController public class BrowserSecurityController { private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @GetMapping("/authentication/require") @ResponseStatus(HttpStatus.UNAUTHORIZED) // 状态码401 public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request,response); if (savedRequest != null) { String targetUrl = savedRequest.getRedirectUrl(); if (StringUtils.endsWithIgnoreCase(targetUrl,".html")) { redirectStrategy.sendRedirect(request,response,"/login.html"); } } return "您访问的资源需要身份认证!"; } } ``` 其中`HttpSessionRequestCache`为Spring Security提供的用于缓存请求的对象,通过调用它的`getRequest`方法可以获取到本次请求的HTTP信息。`DefaultRedirectStrategy`的`sendRedirect`为Spring Security提供的用于处理重定向的方法 **上面代码获取了引发跳转的请求**,根据请求是否以`.html`为结尾来对应不同的处理方法。如果是以`.html`结尾,那么重定向到登录页面,否则返回”访问的资源需要身份认证!”信息,并且HTTP状态码为401(`HttpStatus.UNAUTHORIZED`) 这样当我们访问[http://localhost:8080/hello](http://localhost:8080/hello)的时候页面便会跳转到[http://localhost:8080/authentication/require](http://localhost:8080/authentication/require),并且输出”访问的资源需要身份认证!”,当我们访问[http://localhost:8080/hello.html](http://localhost:8080/hello.html)的时候,页面将会跳转到登录页面 ### 自定义登录成功逻辑 Spring Security有一套默认的处理登录成功和失败的方法:当用户登录成功时,页面会跳转会引发登录的请求,比如在未登录的情况下访问[http://localhost:8080/hello](http://localhost:8080/hello),页面会跳转到登录页,登录成功后再跳转回来;登录失败时则是跳转到Spring Security默认的错误提示页面。 下面我们通过一些自定义配置来替换这套默认的处理机制。 首先,需要自定义`AuthenticationSuccessHandler `接口实现类 ```java @Component public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler { @Resource private ObjectMapper mapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.getWriter().write(mapper.writeValueAsString(authentication)); } } ``` 其中`Authentication`参数既包含了认证请求的一些信息,比如`IP`,请求的`SessionId`等,也包含了用户信息,即前面提到的`User`对象。通过上面这个配置,用户登录成功后页面将打印出`Authentication`对象的信息。 要使这个配置生效,我们还的在`BrowserSecurityConfig`的`configure`中配置它: ```java @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/login") .successHandler(myAuthenticationSuccessHandler) .and() .authorizeRequests() // 授权配置 .antMatchers("/authentication/require","/login.html","/css/*").permitAll() .anyRequest() .authenticated() .and() .csrf().disable(); } ``` 我们将`MyAuthenticationSucessHandler`注入进来,并通过`successHandler`方法进行配置。 这时候重启项目登录后页面将会输出如下`JSON`信息: ```json { "authorities":[ { "authority":"admin" } ], "details":{ "remoteAddress":"0:0:0:0:0:0:0:1", "sessionId":"EBD9FD941653AE07B9485FB3D342E6D0" }, "authenticated":true, "principal":{ "password":null, "username":"admin", "authorities":[ { "authority":"admin" } ], "accountNonExpired":true, "accountNonLocked":true, "credentialsNonExpired":true, "enabled":true }, "credentials":null, "name":"admin" } ``` 像`password`,`credentials`这些敏感信息,`Spring Security`已经将其屏蔽。 除此之外,我们也可以在登录成功后做页面的跳转,修改`MyAuthenticationSucessHandler`: ```java @Component public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{ SavedRequest savedRequest = requestCache.getRequest(request,response); redirectStrategy.sendRedirect(request,response, savedRequest.getRedirectUrl()); } } ``` 跳转指定链接也可以,例如登录成功后跳转到[http://localhost:8080/index](http://localhost:8080/index) ```java @Component public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{ redirectStrategy.sendRedirect(request,response, "/index"); } } ``` 登录成功后,便可以使用`SecurityContextHolder.getContext().getAuthentication()`获取到`Authentication`对象信息。 ```java @GetMapping("index") public Object index(){ return SecurityContextHolder.getContext().getAuthentication(); } ``` 除了通过这种方式获取`Authentication`对象信息外,也可以使用下面这种方式: ```java @GetMapping("index") public Object index(Authentication authentication){ return authentication; } ```
{{< image src="/images/all/image-20230413173140995.png" caption="">}}
### 自定义登录失败逻辑 和自定义登录成功处理逻辑类似,自定义登录失败处理逻辑**首先需要实现**`org.springframework.security.web.authentication.AuthenticationFailureHandler`的`onAuthenticationFailure`方法: ```java @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper mapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(mapper.writeValueAsString(exception.getMessage())); } } ``` > `onAuthenticationFailure`方法的`AuthenticationException`参数是一个抽象类,Spring Security根据登录失败的原因封装了许多对应的实现类。不同的失败原因对应不同的异常,比如用户名或密码错误对应的是`BadCredentialsException`,用户不存在对应的是`UsernameNotFoundException`,用户被锁定对应的是`LockedException`等。 状态码定义为500(`HttpStatus.INTERNAL_SERVER_ERROR.value()`),即系统内部异常。 **然后**在`BrowserSecurityConfig`中配置`MyAuthenticationFailureHandler` ```java @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 .loginPage("/authentication/require") .loginProcessingUrl("/login") .successHandler(myAuthenticationSuccessHandler) // 配置自定义登录成功的handler .failureHandler(myAuthenticationFailureHandler) // 配置自定义登录失败的handler .and() .authorizeRequests() // 授权配置 .antMatchers("/authentication/require","/login.html","/css/*").permitAll() .anyRequest() // 所有请求 .authenticated() .and() .csrf().disable(); // 都需要认证 } ... } ``` 比如输入错误的密码,会返回下面的信息 ``` "用户名或密码错误" ``` ### 添加图形验证码 #### 导入依赖 ```xml org.springframework.social spring-social-config 1.1.6.RELEASE ``` #### 生成验证码 创建验证码对象`ImageCode` ```java @Data 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; } boolean isExpire() { return LocalDateTime.now().isAfter(expireTime); } } ``` `ImageCode`对象包含了三个属性:`image`图片,`code`验证码和`expireTime`过期时间。`isExpire`方法用于判断验证码是否已过期。 接着定义一个`ValidateCodeController`,用于处理生成验证码请求: ```java @RestController public class ValidateCodeController { public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE"; private final SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException{ ImageCode imageCode = ImageCodeUtils.createImageCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode); ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream()); } } ``` 验证码生成类如下: ```java package cc.bnblogs.springsecurity.utils; import cc.bnblogs.springsecurity.pojo.ImageCode; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; /** * 生成图形验证码 */ public class ImageCodeUtils { public static ImageCode createImageCode() { int width = 270; // 验证码图片宽度 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, 100 + 13 * i + 6, 24); } g.dispose(); return new ImageCode(image, sRand.toString(), expireIn); } private static 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); } } ``` `createImageCode`方法用于生成验证码对象,`org.springframework.social.connect.web.HttpSessionSessionStrategy`对象封装了一些处理`Session`的方法,包含了`setAttribute`、`getAttribute`和`removeAttribute`方法,具体可以查看该类的源码。使用`sessionStrategy`将生成的验证码对象存储到`Session`中,并通过`IO`流将生成的图片输出到登录页面上。 要使生成验证码的请求不被拦截,需要在`BrowserSecurityConfig`的`configure`方法中配置免拦截: ```java @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/login") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailureHandler) .and() .authorizeRequests() .antMatchers("/authentication/require", "/login.html", "/css/*", "/code/image").permitAll() // 放行"/code/image"请求 .anyRequest() .authenticated() .and() .csrf().disable(); } ``` 修改`login.html`页面 ```html 登录

账户登录



``` 成功生成验证码!
{{< image src="/images/all/image-20230413200411645.png" caption="">}}
#### 认证流程添加验证码校验 在校验验证码的过程中,可能会抛出各种验证码类型的异常,比如“验证码错误”、“验证码已过期”等 首先自定义一个**验证码异常类** ```java public class ValidateCodeException extends AuthenticationException { private static final long serialVersionUID = -2205230712358933815L; ValidateCodeException(String message) { super(message); } } ``` `Spring Security`实际上是由许多过滤器组成的过滤器链,处理用户登录逻辑的过滤器为`UsernamePasswordAuthenticationFilter`,而验证码校验过程应该是在这个过滤器之前的,即**只有验证码校验通过后才去校验用户名和密码。** 由于`Spring Security`并没有直接提供验证码校验相关的过滤器接口,所以自己定义一个验证码校验的过滤器`ValidateCodeFilter` ```java /** * 本过滤器只会校验一次 */ @Component public class ValidateCodeFilter extends OncePerRequestFilter { @Autowired private AuthenticationFailureHandler authenticationFailureHandler; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { if (StringUtils.equalsIgnoreCase("/login", httpServletRequest.getRequestURI()) && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) { try { validateCode(new ServletWebRequest(httpServletRequest)); } catch (ValidateCodeException e) { authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); } /** * 验证码校验 * @param servletWebRequest * @throws ServletRequestBindingException */ private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE); String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest. getRequest(), "imageCode"); if (StringUtils.isBlank(codeInRequest)) { throw new ValidateCodeException("验证码不能为空!"); } if (codeInSession == null) { throw new ValidateCodeException("验证码不存在!"); } if (codeInSession.isExpire()) { sessionStrategy.removeAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE); throw new ValidateCodeException("验证码已过期!"); } if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) { throw new ValidateCodeException("验证码不正确!"); } sessionStrategy.removeAttribute(servletWebRequest, ValidateCodeController.SESSION_KEY_IMAGE_CODE); } } ``` `ValidateCodeFilter`继承了`org.springframework.web.filter.OncePerRequestFilter`,**该过滤器只会执行一次**。 在`doFilterInternal`方法中我们判断了请求URL是否为`/login`,该路径对应登录`form`表单的`action`路径,请求的方法是否为**`POST`**,是的话进行验证码校验逻辑,否则直接执行`filterChain.doFilter`让代码往下走。当在验证码校验的过程中捕获到异常时,调用Spring Security的校验失败处理器`AuthenticationFailureHandler`进行处理。 **验证码校验逻辑:** 从`Session`中获取了`ImageCode`对象和请求参数`imageCode`(对应登录页面的验证码``框`name`属性),然后进行了各种判断并抛出相应的异常。当验证码过期或者验证码校验通过时,我们便可以删除`Session`中的`ImageCode`属性了。 > 验证码校验过滤器定义好了,怎么才能将其添加到`UsernamePasswordAuthenticationFilter`前面呢? 很简单,只需要在`BrowserSecurityConfig`的`configure`方法中添加些许配置即可,修改后的`configure`方法如下: ```java @Autowired private ValidateCodeFilter validateCodeFilter; @Override protected void configure(HttpSecurity http) throws Exception { // 在检验用户名和密码前先校验验证码 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin()// 表单登录 .loginPage("/authentication/require") .loginProcessingUrl("/login") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailureHandler) .and() .authorizeRequests() // 授权配置 .antMatchers("/authentication/require", "/login.html", "/css/*", "/code/image").permitAll() .anyRequest() // 所有请求 .authenticated() .and() .csrf().disable(); // 都需要认证 } ``` 上面代码中,我们注入了`ValidateCodeFilter`,然后通过`addFilterBefore`方法将`ValidateCodeFilter`验证码校验过滤器添加到了`UsernamePasswordAuthenticationFilter`前面。 重启项目,简单测试一下:
{{< image src="/images/all/image-20230413203621908.png" caption="">}}
{{< image src="/images/all/image-20230413203647260.png" caption="">}}