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

WebClient遇401时刷新AccessToken并重试请求的实现问题

WebClient遇401时刷新AccessToken并重试请求的实现问题

看起来你在处理WebClient遇到401 Unauthorized时自动刷新AccessToken并重试请求的场景,这确实是API调用里非常常见的需求,但Reactor的非阻塞模型很容易让我们踩一些阻塞操作或者请求上下文更新不及时的坑,我来帮你梳理下问题并给出可行的解决方案。

先分析你遇到的两个核心问题

  1. 第一个方案的阻塞错误:你在refreshToken()里用了block()来同步获取新token,这在Reactor的非阻塞IO线程(比如reactor-http-nio-2)里是严格禁止的——这类线程是事件循环线程,阻塞它们会直接破坏反应式模型的性能和稳定性,所以必然会抛出那个错误。

  2. 第二个方案的header未更新问题:你的过滤器在刷新token后,只是把新token存到了tokenSupplier里,但并没有更新重试请求的Authorization header!你创建的newRequest是从原请求复制来的,原请求的header还是旧的(或者空的),所以重试时依然用的是无效的token,自然还是401。

正确的非阻塞实现方案

下面是调整后的完整实现,全程遵循Reactor的非阻塞原则,同时解决并发刷新token的问题:

第一步:修正TokenSupplier,全程非阻塞+线程安全

import java.util.concurrent.atomic.AtomicReference;
import org.springframework.web.reactive.function.client.WebClient;

public class TokenSupplier {
    private final WebClient webClient;
    // 用AtomicReference保证多线程下token的安全更新
    private final AtomicReference<AccessToken> accessTokenRef = new AtomicReference<>();

    public TokenSupplier(ApplicationProperties appProps) {
        this.webClient = WebClient.builder()
                .baseUrl(appProps.getAuthUrl())
                .defaultHeaders(header -> header.setBasicAuth(appProps.getAuthTokenName(), appProps.getAuthTokenPassword()))
                .build();
    }

    // 获取有效token:如果当前token有效直接返回,否则刷新后返回
    public Mono<String> getValidAccessToken() {
        AccessToken currentToken = accessTokenRef.get();
        if (currentToken != null && !isTokenExpired(currentToken)) {
            return Mono.just(currentToken.getAccessToken());
        }
        // 用cache()避免并发刷新:多个请求同时进来时,只执行一次刷新操作
        return refreshToken()
                .map(AccessToken::getAccessToken)
                .cache();
    }

    // 非阻塞刷新token,刷新后更新本地缓存
    private Mono<AccessToken> refreshToken() {
        return webClient.post()
                .body(BodyInserters.fromFormData("grant_type", "client_credentials"))
                .retrieve()
                .bodyToMono(AccessToken.class)
                .doOnNext(token -> {
                    // 这里建议给AccessToken加一个获取时间字段,用于判断过期
                    token.setObtainedTime(System.currentTimeMillis());
                    accessTokenRef.set(token);
                });
    }

    // 根据token的过期时间判断是否有效(需要你的AccessToken类支持expires_in和obtainedTime字段)
    private boolean isTokenExpired(AccessToken token) {
        // 提前10秒刷新,避免token刚好在请求过程中过期
        long expireTime = token.getObtainedTime() + (token.getExpiresIn() - 10) * 1000;
        return System.currentTimeMillis() > expireTime;
    }
}

第二步:重构Client,用过滤器拆分职责+自动重试

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Map;

public class Client {
    private final WebClient webClient;
    private final TokenSupplier tokenSupplier;

    public Client(ApplicationProperties appProps) {
        this.tokenSupplier = new TokenSupplier(appProps);
        this.webClient = WebClient.builder()
                .baseUrl(appProps.getRequestUrl())
                // 过滤器1:自动给请求添加有效的Authorization header
                .filter(addAuthHeaderFilter())
                // 过滤器2:处理401错误,刷新token并重试
                .filter(handle401RetryFilter())
                .build();
    }

    // 如果你在非反应式环境必须用block(),可以保留;如果是反应式环境,建议返回Mono<SomeResponseDTO>
    public SomeResponseDTO getAppProperties() {
        return webClient.post()
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(Map.of("query", "someQuery")))
                .retrieve()
                .bodyToMono(SomeResponseDTO.class)
                .block();
    }

    // 自动添加授权头的过滤器
    private ExchangeFilterFunction addAuthHeaderFilter() {
        return (request, next) -> tokenSupplier.getValidAccessToken()
                .map(token -> ClientRequest.from(request)
                        .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                        .build())
                .flatMap(next::exchange);
    }

    // 处理401并重试的过滤器
    private ExchangeFilterFunction handle401RetryFilter() {
        return (request, next) -> next.exchange(request).flatMap(response -> {
            if (response.statusCode() == HttpStatus.UNAUTHORIZED) {
                // 必须先释放响应体,避免资源泄漏
                return response.releaseBody()
                        // 刷新token(会更新tokenSupplier里的缓存)
                        .then(tokenSupplier.refreshToken())
                        // 重新执行请求,此时addAuthHeaderFilter会用新的token
                        .then(next.exchange(request));
            }
            return Mono.just(response);
        });
    }
}

关键细节说明

  • 线程安全:用AtomicReference存储token,避免多线程下的并发更新问题。
  • 避免重复刷新:getValidAccessToken()里的cache()可以保证多个并发请求触发token刷新时,只执行一次刷新操作,减少不必要的认证服务调用。
  • 职责拆分:两个过滤器分别负责添加授权头和处理401重试,代码更清晰易维护。
  • 非阻塞全程:所有操作都是基于Mono的反应式流,没有使用任何阻塞方法,完全符合Reactor的模型,不会再出现阻塞线程的错误。
  • 提前刷新token:在判断token过期时提前10秒刷新,避免token刚好在请求发送过程中过期导致的401。

备注:内容来源于stack exchange,提问作者user6419217

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.20 12:18:08