Spring WebClient偶发传递失效Access Token问题排查求助
我正在使用Spring Boot 3.0.4搭配Java 17,WebClient在调用API时偶发传递失效的Access Token。
错误场景如下:每日下午4:00会生成新的Access Token,之后在下午4:45又强制生成了新的Access Token,旧Token随即失效。但4:45之后的API调用中,WebClient仍会使用之前的失效Token。按预期Spring应该会在Access Token无效时自动获取新Token,但该问题偶发出现。
Access Token响应示例
{ "access_token": "********-*****-****-****-***********", "scope": "am_application_scope default", "token_type": "Bearer", "expires_in": 20138 }
WebClient配置代码
@Slf4j @Configuration public class Oauth2WebClientConfig { private final Environment env; @Autowired public Oauth2WebClientConfig(Environment env) { this.env = env; } // == Oauth2 Configuration == // == Oauth2 Configuration == @Bean ReactiveClientRegistrationRepository clientRegistration() { ClientRegistration clientRegistration = ClientRegistration .withRegistrationId("custom") .tokenUri(env.getProperty("token-uri")) .clientId(env.getProperty("client-id")) .clientSecret(env.getProperty("client-secret")) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .scope(env.getProperty("scope")) .build(); return new InMemoryReactiveClientRegistrationRepository(clientRegistration); } @Bean ReactiveOAuth2AuthorizedClientService authorizedClientService() { return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistration()); } // == WebFlux Configuration == @Bean WebClient webClient(ReactiveClientRegistrationRepository clientRegistration, ReactiveOAuth2AuthorizedClientService authorizedClientService) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction( new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistration, authorizedClientService)); oauth.setDefaultClientRegistrationId("custom"); return WebClient.builder() .filter(oauth) .filters(exchangeFilterFunctions -> { exchangeFilterFunctions.add(logRequest()); exchangeFilterFunctions.add(logResponse()); }) .build(); } // == Log Request == private ExchangeFilterFunction logRequest() { return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { StringBuilder sb = new StringBuilder("Request: \n") .append(clientRequest.method()) .append(" ") .append(clientRequest.headers()) .append(" ") .append(clientRequest.url()); clientRequest .headers() .forEach((name, values) -> values.forEach(value -> sb .append("\n") .append(name) .append(":") .append(value))); log.info(sb.toString()); return Mono.just(clientRequest); }); } // == Log Response == private ExchangeFilterFunction logResponse() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { StringBuilder sb = new StringBuilder("Response: \n") .append("Status: ") .append(clientResponse.rawStatusCode()) .append(" ") .append(clientResponse.headers()) .append(" ") .append("Body : ") .append(clientResponse.bodyToMono(String.class)); clientResponse .headers() .asHttpHeaders() .forEach((key, value1) -> value1.forEach(value -> sb .append("\n") .append(key) .append(":") .append(value))); log.info(sb.toString()); return Mono.just(clientResponse); }); } // == WebFlux Configuration == }
WebClient调用代码
String response = Objects.requireNonNull(webClient.get() .uri(reqUrl) .header("TRACE_ID", traceId) .retrieve()) .onStatus(HttpStatus::isError, resp -> resp.bodyToMono(String.class).flatMap(error -> Mono.error(new RuntimeException(error)))) .bodyToMono(String.class) .retry(2) .doOnError(error -> log.error("API error has occurred {} : TRACE_ID : {}", error.getMessage(), traceId)) .block(); log.info("response {} : TRACE_ID : {}", response, traceId);
核心原因
授权客户端缓存未感知强制失效:
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager默认仅在Token临近过期(默认提前60秒)或收到API返回的401/403错误时,才会触发Token刷新。但服务端在4:45强制失效旧Token时,客户端缓存的Token还未到过期时间,管理器不会主动刷新,导致继续使用失效Token。错误处理逻辑阻断Token刷新触发:当前调用代码中,
onStatus将所有错误响应包装为RuntimeException,并直接执行retry(2)。这种处理方式跳过了OAuth2过滤器对401错误的识别,重试时只会重复使用旧Token,不会触发刷新逻辑。日志过滤器消耗响应体:
logResponse中直接调用clientResponse.bodyToMono(String.class)会消耗响应体,导致后续的错误处理(包括OAuth2过滤器的Token失效判断)无法读取响应内容,进一步阻断刷新逻辑。
解决方法
1. 配置Token提前刷新时间
给授权客户端管理器设置clockSkew,提前刷新Token,避免服务端强制失效时客户端仍使用旧Token:
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistration, authorizedClientService); manager.setClockSkew(Duration.ofMinutes(10)); // 提前10分钟刷新Token
2. 调整错误处理逻辑,让OAuth2过滤器感知失效
修改onStatus处理,仅在非401错误时抛出异常,保留401错误让OAuth2过滤器处理:
.onStatus(status -> status.isError() && status != HttpStatus.UNAUTHORIZED, resp -> resp.bodyToMono(String.class).flatMap(error -> Mono.error(new RuntimeException(error))))
同时将retry(2)替换为retryWhen,仅在Token刷新成功后重试:
.retryWhen(Retry.backoff(2, Duration.ofSeconds(1)) .filter(e -> e instanceof OAuth2AuthorizationException))
3. 修复日志过滤器的响应体消耗问题
缓存响应体后再进行日志输出,确保后续处理能正常读取:
private ExchangeFilterFunction logResponse() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { return clientResponse.bodyToMono(String.class) .cache() .doOnNext(body -> { StringBuilder sb = new StringBuilder("Response: \n") .append("Status: ").append(clientResponse.rawStatusCode()) .append("\nHeaders: ").append(clientResponse.headers().asHttpHeaders()) .append("\nBody : ").append(body); log.info(sb.toString()); }) .map(body -> clientResponse.mutate().body(Mono.just(body)).build()); }); }
4. 强制刷新Token机制(可选)
如果服务端有Token强制失效的通知渠道,收到通知时调用ReactiveOAuth2AuthorizedClientService.removeAuthorizedClient移除旧授权信息,下一次调用会自动获取新Token:
authorizedClientService.removeAuthorizedClient("custom", OAuth2AuthorizedClientService.OAUTH2_CLIENT_AUTHORIZATION_CONTEXT_CLIENT_ID_ATTRIBUTE_NAME);
内容的提问来源于stack exchange,提问作者praneeth

