Spring 5→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

