Spring WebFlux连续请求时SecurityContextHolder返回null
问题根因
这个问题本质是响应式编程模型下上下文传播机制和传统Servlet线程模型不兼容导致的:
- Spring Security在Servlet栈下默认用
ThreadLocal存储SecurityContext,这种机制的特点是上下文数据和当前执行线程绑定,只有同一个线程执行的代码才能读到对应数据。 - 你单独调用接口时,WebFlux的安全过滤器链会在接收请求的Reactor NIO线程上把安全上下文绑定到当前线程的
ThreadLocal,第一个WebClient的拦截器执行时刚好在这个线程上,能正常读取到JWT令牌,所以调用正常。 - 嵌套调用第二个WebClient时,内部请求的发送、拦截器执行逻辑是在第一个接口响应返回后的异步回调线程上执行的,这个回调线程没有绑定过当前请求的
SecurityContext,SecurityContextHolder.getContext()自然返回null触发空指针。 - 额外提一句,你最初写的链式调用还有个逻辑问题:用
map操作符处理返回Flux<Promotion>的方法,最终得到的是Flux<Flux<Promotion>>嵌套流,没有做打平处理,就算解决了上下文问题也拿不到预期的促销对象。
可行解决方案
方案1:使用响应式安全上下文(官方推荐)
完全放弃依赖ThreadLocal的SecurityContextHolder,改用Spring Security为WebFlux提供的ReactiveSecurityContextHolder,它会把安全上下文存储在Reactor自身的上下文结构中,这个结构是和流绑定的,不会因为线程切换丢失,会自动沿着整个操作符链传播。
- 首先修改WebClient的拦截器逻辑,不要从
SecurityContextHolder读令牌,改为从响应式上下文读取:
public ExchangeFilterFunction jwtAuthFilter() { return ExchangeFilterFunction.ofRequestProcessor(request -> ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .map(auth -> (String) auth.getCredentials()) // 取出JWT令牌 .map(token -> ClientRequest.from(request) .header("Authorization", "Bearer " + token) .build()) .defaultIfEmpty(request) ); }
- 把业务流中的
map替换为flatMap打平嵌套的响应式流,flatMap会自动订阅内部流并完成上下文传播:
productService.getProducts() .flatMap(product -> promotionService.getPromotions(product)) .subscribe(promotion -> System.out.println(promotion.getName()));
直接嵌套WebClient调用的写法也按同样规则修改即可:
productWebClient.post() .bodyValue(Map.of()) .retrieve() .bodyToFlux(Product.class) .flatMap(product -> promotionWebClient.post() .bodyValue(Map.of("id", product.getId())) .retrieve() .bodyToFlux(Promotion.class) ) .subscribe(System.out::println);
方案2:显式传递令牌(兼容旧拦截器逻辑)
如果暂时不想修改现有拦截器的实现,可以在流的最开始、还能正常拿到安全上下文的位置提前取出JWT令牌,后续所有WebClient调用都显式传入令牌,完全绕开异步线程读ThreadLocal的逻辑:
// 先在流入口处拿到令牌 ReactiveSecurityContextHolder.getContext() .map(ctx -> (String) ctx.getAuthentication().getCredentials()) .flatMapMany(token -> productWebClient.post() .bodyValue(Map.of()) .header("Authorization", "Bearer " + token) .retrieve() .bodyToFlux(Product.class) .flatMap(product -> promotionWebClient.post() .bodyValue(Map.of("id", product.getId())) .header("Authorization", "Bearer " + token) .retrieve() .bodyToFlux(Promotion.class) ) ) .subscribe(promotion -> System.out.println(promotion.getName()));
注意:WebFlux栈下尽量不要依赖ThreadLocal存储请求级别的上下文数据,只要存在异步回调、线程切换,就必然会出现上下文丢失的问题,所有上下文传递都应该基于Reactor Context实现。
内容的提问来源于stack exchange,提问作者Ali Ben Messaoud
相关产品推荐
相关产品推荐

