Spring Boot3升级后自定义OTP认证SecurityContext未持久化问题
升级到Spring Boot 3(对应Spring Security 6)后,原本在Spring Security 5下正常运行的自定义OTP认证出现异常:认证流程能完整执行(包括OtpProvider正确创建UserDetails),但认证对象没存入SecurityContext,导致后续所有请求返回401。
环境
- Spring Boot 3.x
- Spring Security 6.x
现有安全配置代码
@Bean("otpSecurityFilterConfiguration") @Order(1) public SecurityFilterChain otpSecurityFilterChainConfiguration(HttpSecurity http) throws Exception { OtpAuthenticationFilter otpAuthenticationFilter = new OtpAuthenticationFilter(createOtpAuthenticationManager(), authenticationSuccessHandler, authenticationFailureHandler); http .securityMatcher(new AntPathRequestMatcher("/**")) .addFilterBefore(otpAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .formLogin(form -> form.loginProcessingUrl("/login")) .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) .exceptionHandling(handler -> handler.authenticationEntryPoint(otpAuthenticationEntryPoint)) .authorizeHttpRequests(requests -> requests .requestMatchers("/auth/sendOtp").permitAll() .anyRequest().authenticated()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .headers(headers -> headers .contentTypeOptions(Customizer.withDefaults())); return http.build(); } private AuthenticationManager createOtpAuthenticationManager() { return new ProviderManager(otpAuthProvider); }
补充信息
- 自定义
OtpAuthenticationFilter继承AbstractAuthenticationProcessingFilter,负责捕获/login请求生成认证令牌 - 未手动操作SecurityContext,也没重写
successfulAuthentication方法 - 临时用
.securityContext((securityContext) -> securityContext.requireExplicitSave(false))解决,但这不符合Spring Security 6的显式持久化规范,想找合规方案
问题原因
Spring Security 6默认开启requireExplicitSave(true),要求必须显式把SecurityContext保存到SecurityContextRepository。虽然AbstractAuthenticationProcessingFilter的successfulAuthentication方法应该自动处理保存,但问题出在你手动创建的ProviderManager没有绑定当前HttpSecurity配置中的SecurityContextRepository——导致认证成功后无法触发自动保存逻辑。
正确解决方法
方法1:用HttpSecurity提供的AuthenticationManager(推荐)
别手动实例化ProviderManager,改为从HttpSecurity中获取关联的AuthenticationManager,同时确保你的otpAuthProvider被注册为Spring Bean:
@Bean("otpSecurityFilterConfiguration") @Order(1) public SecurityFilterChain otpSecurityFilterChainConfiguration(HttpSecurity http) throws Exception { // 获取HttpSecurity自带的AuthenticationManager AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); OtpAuthenticationFilter otpAuthenticationFilter = new OtpAuthenticationFilter(authenticationManager, authenticationSuccessHandler, authenticationFailureHandler); // 其余配置保持不变 http .securityMatcher(new AntPathRequestMatcher("/**")) .addFilterBefore(otpAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .formLogin(form -> form.loginProcessingUrl("/login")) .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) .exceptionHandling(handler -> handler.authenticationEntryPoint(otpAuthenticationEntryPoint)) .authorizeHttpRequests(requests -> requests .requestMatchers("/auth/sendOtp").permitAll() .anyRequest().authenticated()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .headers(headers -> headers .contentTypeOptions(Customizer.withDefaults())); return http.build(); } // 确保otpAuthProvider被注册为Bean @Bean public AuthenticationProvider otpAuthProvider() { return new OtpAuthProvider(); }
方法2:给手动创建的ProviderManager绑定SecurityContextRepository
如果必须自己创建ProviderManager,把HttpSecurity里的SecurityContextRepository绑定进去:
@Bean("otpSecurityFilterConfiguration") @Order(1) public SecurityFilterChain otpSecurityFilterChainConfiguration(HttpSecurity http) throws Exception { // 获取默认的SecurityContextRepository SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); ProviderManager otpAuthenticationManager = new ProviderManager(otpAuthProvider); // 绑定上下文仓库到ProviderManager otpAuthenticationManager.setSecurityContextRepository(securityContextRepository); OtpAuthenticationFilter otpAuthenticationFilter = new OtpAuthenticationFilter(otpAuthenticationManager, authenticationSuccessHandler, authenticationFailureHandler); // 其余配置不变 return http.build(); }
方法3:在过滤器中显式保存SecurityContext
如果前两种方法不合适,可以重写OtpAuthenticationFilter的successfulAuthentication方法,手动保存上下文:
public class OtpAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final SecurityContextRepository securityContextRepository; // 构造器注入SecurityContextRepository public OtpAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler, SecurityContextRepository securityContextRepository) { super(new AntPathRequestMatcher("/login", "POST")); setAuthenticationManager(authenticationManager); setAuthenticationSuccessHandler(successHandler); setAuthenticationFailureHandler(failureHandler); this.securityContextRepository = securityContextRepository; } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authResult); // 显式保存上下文到仓库 securityContextRepository.saveContext(context, request, response); SecurityContextHolder.setContext(context); // 调用父类方法执行后续逻辑 super.successfulAuthentication(request, response, chain, authResult); } // 其余方法实现... }
内容的提问来源于stack exchange,提问作者patryks

