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

Spring 5→6迁移后自定义安全组件无法持久化Principal问题

Spring Boot 3.3.4(Spring 6)迁移后登录成功但后续接口无法获取Principal问题排查与解决

问题描述

将系统从Spring 5迁移至Spring Boot 3.3.4(基于Spring 6)后,出现如下异常情况:

  • 调用/login接口认证正常,返回200,自定义AuthenticationProvider返回的UsernamePasswordAuthenticationToken有效
  • 访问/user等依赖Principal的接口时,参数user为null

怀疑问题出在SecurityConfig配置,尤其是自定义XYZLibraryAuthenticationFilter、AuthenticationManager相关代码(Spring 5到6的配置方式有较大变化)。目前已实现Provider的supports方法:

@Override
public boolean supports(Class<?> authentication) {
    return authentication.equals(XYZLibraryAuthenticationToken.class);
}

调整过滤器链顺序后问题仍未解决,寻求排查思路与解决方案。

核心代码

AuthenticationController关键代码

@RequestMapping(value = "/user", method = RequestMethod.GET)
public UserResponse user(Principal user) {
    UserResponse response = new UserResponse();
    response.setCode(CODE_SUCCESS);
    response.setStatus(SUCCESS_STATUS);
    response.setData(user);
    return response;
}

XYZLibrarySecurityConfig核心配置

@Configuration
@EnableWebSecurity(debug = true)
public class XYZLibrarySecurityConfig {

    private final XYZLibraryAuthenticationProvider authProvider;
    private final XYZLibraryConfig properties;

    public XYZLibrarySecurityConfig(@Lazy XYZLibraryAuthenticationProvider authProvider, XYZLibraryConfig properties) {
        this.authProvider = authProvider;
        this.properties = properties;
    }
    
    @Bean
    public XYZLibraryAuthenticationFilter authenticationFilter(AuthenticationManager authenticationManager) {
        XYZLibraryAuthenticationFilter filter = new XYZLibraryAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(authenticationFailureHandler());
        filter.setSessionAuthenticationStrategy(authStrategy());

        return filter;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.authenticationProvider(authProvider);
        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authenticationProvider(authProvider)
            .addFilterBefore(authenticationFilter(authenticationManager(http)), UsernamePasswordAuthenticationFilter.class)
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(authenticationEntryPoint()))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/rest/authentication/authenticate").permitAll()
                .requestMatchers("/rest/authentication/user").permitAll()
                .requestMatchers("/rest/authentication/forgotPassword").permitAll()
                .requestMatchers("/rest/authentication/changePassword").permitAll()
                .requestMatchers("/rest/authentication/passwordRules").permitAll()
                .requestMatchers("/rest/authentication/helpContact").permitAll()
                .requestMatchers("/rest/authentication/user-message").denyAll()
                .requestMatchers("/rest/system-check/check").permitAll()
                .requestMatchers("/rest/admin/**").hasAnyRole("Administrator", "XYZ Complete Editor")
                .requestMatchers("/rest/**").hasAnyRole("User", "Administrator", "XYZ Limited User", "XYZ Limited Submitter", "XYZ Complete User", "XYZ Complete Editor")
                .anyRequest().authenticated()
            )
            .formLogin(formLogin -> formLogin.permitAll())
            .logout(logout -> logout
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true)
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
            )
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; frame-ancestors 'self'; form-action 'self';")
                )
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000)
                )
            )
            .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> {
                if (properties.getCsrf().getEnabled().booleanValue()) {
                    if (properties.getCsrf().getUseCookie().booleanValue()) {
                        CookieCsrfTokenRepository trep = (CookieCsrfTokenRepository) XYZLibraryCookieRepository.withHttpOnlyFalse();
                        trep.setCookiePath("/");
                        csrf.csrfTokenRepository(trep);
                    }
                } else {
                    csrf.disable();
                }
            });

        http.addFilterBefore(expiredSessionFilter(), SessionManagementFilter.class);
        http.addFilterAfter(new RequestAuditingFilter(), BasicAuthenticationFilter.class);

        return http.build();
    }
}

排查思路与解决方案

1. 检查认证成功后的Authentication存储逻辑

Spring 6中,启用SessionCreationPolicy.STATELESS无状态会话模式时,认证成功后的Authentication不会自动存入Session。需确保自定义过滤器XYZLibraryAuthenticationFilter在认证成功后:

  • 将Authentication对象设置到SecurityContextHolder中
  • 生成并返回认证凭证(如JWT Token)
  • 新增全局过滤器,在后续请求中解析凭证并将Authentication重新放入SecurityContextHolder

2. 修正AuthenticationManager配置方式

Spring 6推荐通过全局AuthenticationConfiguration构建认证管理器,替代从HttpSecurity获取共享对象的方式,修改authenticationManager Bean定义:

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}

同时确保XYZLibraryAuthenticationProvider被正确注册:

@Bean
public AuthenticationManagerBuilder configureAuthenticationManagerBuilder(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authProvider);
    return auth;
}

3. 验证supports方法匹配逻辑

确认过滤器提交给认证管理器的Token类型与supports方法匹配:如果过滤器实际提交的是UsernamePasswordAuthenticationToken,需调整方法覆盖该类型:

@Override
public boolean supports(Class<?> authentication) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication) 
            || XYZLibraryAuthenticationToken.class.isAssignableFrom(authentication);
}

4. 检查SecurityContextHolder策略与上下文传播

  • 显式设置SecurityContextHolder存储策略为MODE_THREADLOCAL,确保线程隔离:
static {
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_THREADLOCAL);
}
  • 新增请求过滤器,在请求开始时初始化SecurityContext,请求结束时清理上下文,避免线程污染。

5. 调试过滤器执行顺序

利用@EnableWebSecurity(debug = true)开启调试模式,查看过滤器链执行顺序,确保自定义认证过滤器和上下文恢复过滤器在正确的阶段执行。

内容的提问来源于stack exchange,提问作者aohm1989

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 00:30:17