应使用ScopedValue(JDK 21+)结合ReactiveSecurityContextHolder传递权限上下文,因其支持虚拟线程自动传播、不可篡改且轻量;禁用ThreadLocal,通过请求头透传上下文至下游服务。

在反应式网关中,不能依赖传统 ThreadLocal 传递权限上下文——虚拟线程不继承父线程的 ThreadLocal 值,且响应式链路(如 Mono/Flux)涉及多次线程切换。真正可行的方式是结合 ScopedValue(JDK 21+)与 ReactiveSecurityContextHolder,构建轻量、自动传播、不可篡改的上下文链路。
用 ScopedValue 封装用户身份与权限元数据
ScopedValue 是专为虚拟线程设计的上下文载体,比 ThreadLocal 更安全、更轻量,且能自动跨挂起/恢复点传递:
- 定义不可变上下文记录类:
record RequestContext(String userId, String tenantId, Setauthorities) {} - 声明全局 ScopedValue 实例:
public static final ScopedValueCURRENT_CONTEXT = ScopedValue.newInstance(); - 在网关入口(如 WebFilter)中注入上下文:
ScopedValue.where(CURRENT_CONTEXT, context).run(() -> chain.filter(exchange))
与 ReactiveSecurityContextHolder 协同使用
Spring Security 7.0 的响应式安全模型默认基于 Reactor 的 ContextView,但该上下文不跨服务调用边界。需将 ScopedValue 中的权限信息同步到安全上下文:
- 在认证成功后,从 JWT 解析出权限,并写入 ScopedValue 和 ReactiveSecurityContextHolder:
ReactiveSecurityContextHolder.getContext().map(ctx -> { ctx.setAuthentication(auth); return ctx; }) - 下游微服务通过网关透传的请求头(如
X-Auth-Context)重建 ScopedValue,避免重复鉴权 - 禁止在业务逻辑中直接读取
ReactiveSecurityContextHolder.getContext(),应统一走CURRENT_CONTEXT.get()获取当前虚拟线程绑定的权限快照
规避常见反模式:ThreadLocal + 虚拟线程混用
即使显式设置 SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL),也无法保证在 WebFlux + 虚拟线程组合下上下文稳定——因为 Reactor 的调度器可能将子任务派发到不同 Carrier 线程上:
- 不要在 Filter 或 Handler 中手动 set/remove ThreadLocal,这会引发内存泄漏或上下文污染
- 禁用所有基于 ThreadLocal 的日志 MDC 自动继承(如 Logback 的
AsyncAppender),改用 ScopedValue + 自定义VirtualThreadMDCAdapter - 若需兼容旧组件,用
ContextSnapshot.captureAll().run(() -> {...})显式捕获并恢复上下文,而非依赖隐式继承
网关层权限上下文透传实践
反应式网关(如 Spring Cloud Gateway)需在转发前将 ScopedValue 中的关键字段注入请求头,供下游服务还原:
- 在
GlobalFilter中获取当前上下文:RequestContext ctx = CURRENT_CONTEXT.get(); - 构造透传头:
exchange.mutate().request(builder -> builder.headers(h -> { h.set("X-User-Id", ctx.userId()); h.set("X-Tenant-Id", ctx.tenantId()); h.set("X-Authz", String.join(",", ctx.authorities())); })) - 下游服务收到后,用相同 key 的 ScopedValue 重建上下文,不依赖解析 JWT 或查库,降低延迟











