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

Spring Boot 2.7升级后跨线程请求中Access Token无法传递问题

问题:Spring Boot 2.7.3升级后,线程切换时WebClient无法携带OIDC 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 03:01:21