You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot3升级后自定义OTP认证SecurityContext未持久化问题

问题:Spring Security 6自定义OTP认证成功后SecurityContext未持久化,后续请求401

升级到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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 03:36:12