Spring Boot 3迁移后@PreAuthorize注解失效问题排查求助
问题背景
我们正将应用从Java 8 + Spring Boot 2迁移至Java 17 + Spring Boot 3,多数功能正常,但带有@PreAuthorize注解的API调用出现401认证失败。移除@PreAuthorize注解后接口可正常访问,且日志显示权限信息正确。
配置代码
@Configuration @RequiredArgsConstructor @EnableWebSecurity(debug = true) @EnableMethodSecurity public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); http.csrf().disable(); http.cors(); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeHttpRequests() .requestMatchers("/api/**") .authenticated() .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(new JwtAuthenticationConverter()); return http.build(); } @Bean public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri( properties.getJwt().getJwkSetUri()).build(); return jwtDecoder; } } class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { @Override public AbstractAuthenticationToken convert(Jwt jwt) { return new JwtAuthenticationToken(jwt, new JwtAuthoritiesConverter().convert(jwt)); } } class JwtAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> { @Override public Collection<GrantedAuthority> convert(Jwt jwt) { Collection<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(); grantedAuthorities.add(new SimpleGrantedAuthority("SOME_AUTHORITY")); return grantedAuthorities; } }
示例接口方法
@PreAuthorize("hasAnyAuthority('SOME_AUTHORITY')") @GetMapping("/someObjects") APIResponse<SomeObject> testMethod(JwtAuthenticationToken principal){ List<GrantedAuthority> principalAuthorities= principal.getAuthorities().stream().collect(Collectors.toList()); log.info("Authorities: " + principalAuthorities.toString()); List<GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().collect(Collectors.toList()); log.info("Security Context" + authorities.toString()); return APIResponse.singleton(new SomeObject()); }
现象与日志
移除@PreAuthorize注解后(正常访问)
2023-06-28T09:48:27,618 INFO SomeController::testMethod(223): Authorities: [SOME_AUTHORITY]
2023-06-28T09:48:27,618 INFO SomeController: Security Context[SOME_AUTHORITY]
添加@PreAuthorize注解后(返回401)
2023-06-28T11:27:13,166 DEBUG o.s.s.o.s.r.w.a.BearerTokenAuthenticationFilter::doFilterInternal(143): Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@eb761cc7, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[SOME_AUTHORITY]]
...
2023-06-28T11:27:13,174 TRACE o.s.s.w.a.ExceptionTranslationFilter::handleAuthenticationException(184): Sending to authentication entry point since authentication failed
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
排查方向与原因解释
原因解释:SecurityContextHolder策略差异
MODE_THREADLOCAL:SecurityContext绑定到当前线程,Spring Security过滤器链和@PreAuthorize方法级权限校验在同一线程执行,能正常获取上下文。MODE_INHERITABLETHREADLOCAL:原本用于让子线程继承父线程的SecurityContext,但Spring Boot 3(基于Spring Framework 6)中方法级安全校验的线程模型发生变化。当使用@PreAuthorize时,权限校验可能在不同线程执行,而InheritableThreadLocal的继承逻辑未覆盖该场景(如代理创建的线程上下文传递问题),导致校验线程无法获取父线程已设置的Authentication对象,抛出AuthenticationCredentialsNotFoundException。
排查方向
- 检查方法级安全代理机制:Spring Boot 3默认使用CGLIB代理,可尝试显式配置
@EnableMethodSecurity(proxyTargetClass = false)切换为JDK动态代理,验证是否解决问题。 - 验证异步线程上下文传递:若应用存在异步处理,确认
TaskExecutor是否配置了上下文传递,Spring Boot 3中TaskExecutor默认上下文传递行为可能调整,需确保SecurityContext能传递到异步线程。 - 确认过滤器链执行顺序:检查
BearerTokenAuthenticationFilter是否在方法级安全拦截器前执行,保证SecurityContext在校验前已正确设置。 - 对比Spring Security版本差异:Spring Security 6在SecurityContext管理、方法安全校验流程上有调整,查看官方文档中
@EnableMethodSecurity的配置细节,确认securedEnabled、jsr250Enabled等属性默认值是否变化。 - 调试线程ID:在
@PreAuthorize方法和过滤器中添加线程ID日志,对比两者线程ID是否一致,确认是否为线程上下文传递问题。
内容的提问来源于stack exchange,提问作者Chris G

