Spring Boot 2.7升级后跨线程请求中Access Token无法传递问题
我们的Spring Boot应用集成OIDC作为授权码流客户端,通过ServletOAuth2AuthorizedClientExchangeFilterFunction配置WebClient,实现将已认证用户的Access Token传递给下游资源服务器。但从Spring Boot 2.6.7升级到2.7.3后发现:当请求调度到原Servlet线程(http-nio-*)之外的Reactor线程(reactor-http-nio-*)时,出站请求不再携带Authentication头(即Access Token)。
代码复现
public class MyController { public Mono<ProductSearchResult> searchByName(SearchProductsQuery query) { List<String> sortOrder = new ArrayList<>(); System.out.println("--OUTER: " + Thread.currentThread().getName()); return resourceServere.searchByName(query) .doOnNext(searchResponse -> sortOrder.addAll(searchResponse.getIds())) .flatMap(searchResponse -> Mono.zip( getSomething(searchResponse.getIds()), getSomethingElse(searchResponse.getIds()), Mono.just(searchResponse.pagination()))) .map(tuple3 -> SearchResultMapper.map(tuple3.getT1(), tuple3.getT2(), tuple3.getT3())); } private Mono<List<String>> getSomething(List<String> ids) { System.out.println("--INNER: " + Thread.currentThread().getName()); if (ids.isEmpty()) { return Mono.just(new ArrayList<>()); } return otherResourceServerClient.getStuff(ids); } }
线程打印结果
--OUTER: http-nio-8080-exec-6 --INNER: reactor-http-nio-3
调试发现,http-nio线程的请求上下文包含Authentication信息,但切换到reactor-http线程后,上下文丢失,导致ServletOAuth2AuthorizedClientExchangeFilterFunction无法获取到Access Token添加到请求头。项目依赖包含spring-boot-starter-webflux、spring-boot-starter-web和spring-boot-starter-security。
原因分析
Spring Security 5.6(对应Spring Boot 2.7.x)对混合Servlet+Reactor环境下的上下文传播机制做了调整:默认的SecurityContextHolderStrategy逻辑变化,导致子线程(Reactor线程)无法继承父Servlet线程的SecurityContext,最终ServletOAuth2AuthorizedClientExchangeFilterFunction无法获取认证信息生成请求头。
修复步骤
1. 配置可继承的线程本地SecurityContext策略
在application.properties中添加配置:
spring.security.strategy=MODE_INHERITABLETHREADLOCAL
或通过Java代码配置:
@Configuration public class SecurityConfig { @Bean public SecurityContextHolderStrategy securityContextHolderStrategy() { return SecurityContextHolder.createEmptyContextHolderStrategy(SecurityContextHolder.Mode.INHERITABLETHREADLOCAL); } }
该配置确保Reactor线程能继承Servlet线程的SecurityContext。
2. 手动绑定SecurityContext到Reactor上下文
若上述配置不生效,可在Mono链中手动将Servlet线程的SecurityContext绑定到Reactor上下文:
public Mono<ProductSearchResult> searchByName(SearchProductsQuery query) { // 获取当前Servlet线程的SecurityContext SecurityContext securityContext = SecurityContextHolder.getContext(); List<String> sortOrder = new ArrayList<>(); System.out.println("--OUTER: " + Thread.currentThread().getName()); return resourceServere.searchByName(query) .doOnNext(searchResponse -> sortOrder.addAll(searchResponse.getIds())) // 绑定SecurityContext到Reactor上下文 .contextWrite(ctx -> ctx.put(SecurityContext.class, securityContext)) .flatMap(searchResponse -> Mono.zip( getSomething(searchResponse.getIds()), getSomethingElse(searchResponse.getIds()), Mono.just(searchResponse.pagination()))) .map(tuple3 -> SearchResultMapper.map(tuple3.getT1(), tuple3.getT2(), tuple3.getT3())); }
同时修改WebClient配置,让过滤器优先从Reactor上下文获取SecurityContext:
@Bean public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); // 自定义Authentication获取逻辑,优先从Reactor上下文取 oauth2Client.setAuthenticationConverter(exchange -> { return exchange.getClientRequest().attributes() .values().stream() .filter(attr -> attr instanceof ServerWebExchange) .map(attr -> ((ServerWebExchange) attr).getAttribute(SecurityContext.class)) .map(SecurityContext::getAuthentication) .findFirst() .orElseGet(() -> SecurityContextHolder.getContext().getAuthentication()); }); return WebClient.builder() .apply(oauth2Client.oauth2Configuration()) .build(); }
3. 使用ReactiveSecurityContextHolder传递认证信息
将Servlet线程的Authentication绑定到Reactive安全上下文,确保Reactor链中可访问:
public Mono<ProductSearchResult> searchByName(SearchProductsQuery query) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); List<String> sortOrder = new ArrayList<>(); System.out.println("--OUTER: " + Thread.currentThread().getName()); return ReactiveSecurityContextHolder.withAuthentication(authentication) .flatMap(ctx -> resourceServere.searchByName(query) .doOnNext(searchResponse -> sortOrder.addAll(searchResponse.getIds())) .flatMap(searchResponse -> Mono.zip( getSomething(searchResponse.getIds()), getSomethingElse(searchResponse.getIds()), Mono.just(searchResponse.pagination()))) .map(tuple3 -> SearchResultMapper.map(tuple3.getT1(), tuple3.getT2(), tuple3.getT3()))); }
社区反馈
确实有不少开发者在Spring Boot 2.7.x升级后遇到混合环境下的SecurityContext跨线程丢失问题,上述方案均为社区验证有效的解决方式。
内容的提问来源于stack exchange,提问作者davidj

