Spring Boot WebClient重试401刷新AccessToken时,外部调用中Security Context为Null的解决方法
Spring Boot WebClient重试401刷新AccessToken时,外部调用中Security Context为Null的解决方法
兄弟,我前阵子刚踩过几乎一模一样的坑!你遇到的问题本质上是传统的SecurityContextHolder是线程绑定的,而WebClient的重试操作会在新的订阅线程上执行,原线程的安全上下文根本传不过去,所以你在authService.getToken()里拿不到Authentication对象太正常了。结合你给出的代码,我给你一套落地性很强的解决步骤:
第一步:把同步上下文获取改成反应式上下文
首先,你得把authService里的同步上下文获取逻辑彻底换掉——反应式流里根本不能用SecurityContextHolder.getContext()这种线程绑定的方式,必须用ReactiveSecurityContextHolder。
把原来的同步方法:
private String getToken() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 原有获取token的逻辑 }
改成反应式版本:
public Mono<String> getToken() { return ReactiveSecurityContextHolder.getContext() // 从反应式上下文中拿Authentication .map(SecurityContext::getAuthentication) // 如果上下文为空,直接抛出异常(根据你的业务调整) .switchIfEmpty(Mono.error(() -> new IllegalStateException("当前无认证上下文,请检查请求链路"))) .flatMap(authentication -> { // 这里放你原来的获取token逻辑,比如用authentication的信息调用内部认证接口 return internalAuthService.getTokenForUser(authentication.getName()); }); }
注意:这个方法的返回值必须是Mono<String>(或者对应你的返回类型),不能再用同步的方式写,否则上下文还是拿不到。
第二步:在重试逻辑中传播反应式上下文
接下来要处理WebClient的retryWhen逻辑——你需要在重试操作执行前,先捕获当前流中的反应式安全上下文,然后在重试的回调里把这个上下文“写入”到新的子流中,确保obtainOAuthToken能访问到原上下文。
首先调整你的重试判断逻辑,把401也加进去(毕竟你是要刷新token,401才是核心触发场景):
private boolean isRetryableError(RetryContext<Object> retryContext) { if (retryContext.exception() instanceof WebClientResponseException) { WebClientResponseException ex = (WebClientResponseException) retryContext.exception(); return ex.getStatusCode().is5xxServerError() || ex.getStatusCode() == HttpStatus.UNAUTHORIZED; } return false; }
然后修改WebClient的retryWhen代码,关键是提前捕获上下文并在重试时传播:
// 先在WebClient调用的上游流中捕获当前的反应式安全上下文(一定要在原流里拿,不能在重试回调里拿!) Mono<SecurityContext> currentSecurityContext = ReactiveSecurityContextHolder.getContext().cache(); webClient.post() .uri("/your-target-api") .headers(headers -> headers.add(HttpHeaders.AUTHORIZATION, initialTokenHeader)) .retrieve() .bodyToMono(YourResponse.class) .retryWhen(Retry.max(3) .filter(this::isRetryableError) // 用doBeforeRetryAsync替代同步的doBeforeRetry,确保在反应式流中执行 .doBeforeRetryAsync(retrySignal -> // 把捕获到的原上下文写入到当前重试的子流中 currentSecurityContext.flatMap(context -> obtainOAuthToken(authProp) .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(context))) .doOnNext(tokenHeader -> { // 刷新请求头里的token authHeaders.set(HttpHeaders.AUTHORIZATION, tokenHeader); }) ) ) );
同时,你需要把obtainOAuthToken也改成返回Mono<String>的反应式方法,内部调用上面修改后的authService.getToken():
public Mono<String> obtainOAuthToken(AuthProp authProp) { return authService.getToken() .map(token -> "Bearer " + token); // 拼接成标准的Authorization头格式 }
关键注意点
- 绝对不要在反应式流里用
block()强制同步,除非你有绝对必要的业务场景——强制同步会打破反应式上下文的传播,还可能导致线程阻塞问题。 - 如果你在其他地方还有同步的
SecurityContextHolder调用,一定要全部替换成ReactiveSecurityContextHolder,否则上下文传播链条会断。
内容来源于stack exchange
相关产品推荐
相关产品推荐

