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

使用HttpClient与自定义DelegatingHandler检测Unauthorized请求

解决DelegatingHandler中并发请求重复刷新令牌的问题

这个场景我之前做项目时也踩过坑——并发请求同时返回401时,会齐刷刷触发刷新逻辑,既浪费资源还可能触发令牌服务的限流。核心解决思路就是加个异步锁,确保同一时间只有一个请求去执行刷新令牌操作,其他请求等着用新生成的令牌即可。

下面是具体的实现方案:

1. 给Handler添加异步锁

在你的DelegatingHandler子类里,维护一个SemaphoreSlim实例(初始计数设为1),用来实现异步环境下的互斥访问:

public class AuthenticationHandler : DelegatingHandler
{
    // 异步锁,确保同一时间仅一个请求执行刷新逻辑
    private readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1);
    private string _currentAccessToken;
    private string _refreshToken;

    // 构造函数注入所需服务(比如令牌存储、身份认证服务等)
    public AuthenticationHandler(/* 按需注入你的服务,比如ITokenStorage */)
    {
        // 从缓存/本地存储初始化令牌
        _currentAccessToken = /* 初始访问令牌 */;
        _refreshToken = /* 初始刷新令牌 */;
    }
}

2. 重写SendAsync,处理401与令牌刷新

在SendAsync方法中,先正常发送请求,遇到401时进入刷新逻辑,通过_refreshLock.WaitAsync()等待锁资源:

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    // 给请求带上当前访问令牌
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _currentAccessToken);

    var response = await base.SendAsync(request, cancellationToken);

    // 检测到未授权,尝试刷新令牌并重试
    if (response.StatusCode == HttpStatusCode.Unauthorized)
    {
        await _refreshLock.WaitAsync(cancellationToken);
        try
        {
            // 双重检查令牌:避免等待锁期间已有其他请求完成刷新
            if (_currentAccessToken != request.Headers.Authorization.Parameter)
            {
                // 令牌已更新,直接用新令牌重试请求
                var clonedRequest = await CloneRequest(request);
                clonedRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _currentAccessToken);
                return await base.SendAsync(clonedRequest, cancellationToken);
            }

            // 执行刷新令牌操作
            var newTokens = await RefreshAccessTokenAsync(cancellationToken);
            if (newTokens == null)
            {
                // 刷新失败,返回原401或引导用户重新登录
                return response;
            }

            // 更新全局令牌
            _currentAccessToken = newTokens.AccessToken;
            _refreshToken = newTokens.RefreshToken;

            // 克隆原请求(原请求Content已被读取,无法重复发送)
            var retryRequest = await CloneRequest(request);
            retryRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _currentAccessToken);

            // 用新令牌重试请求
            return await base.SendAsync(retryRequest, cancellationToken);
        }
        finally
        {
            // 释放锁,允许其他请求继续
            _refreshLock.Release();
        }
    }

    return response;
}

3. 辅助方法:克隆请求与刷新令牌

需要两个关键辅助方法来完善逻辑:

  • CloneRequest:HTTP请求的Content流只能读取一次,重试时必须克隆新请求:
private async Task<HttpRequestMessage> CloneRequest(HttpRequestMessage request)
{
    var clonedRequest = new HttpRequestMessage(request.Method, request.RequestUri);

    // 复制请求头
    foreach (var header in request.Headers)
    {
        clonedRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
    }

    // 复制请求内容(如果存在)
    if (request.Content != null)
    {
        var contentBytes = await request.Content.ReadAsByteArrayAsync();
        clonedRequest.Content = new ByteArrayContent(contentBytes);
        foreach (var header in request.Content.Headers)
        {
            clonedRequest.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
    }

    return clonedRequest;
}
  • RefreshAccessTokenAsync:封装刷新令牌的具体逻辑(比如调用身份服务的token端点):
private async Task<Tokens> RefreshAccessTokenAsync(CancellationToken cancellationToken)
{
    var client = new HttpClient();
    var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://your-identity-server/token")
    {
        Content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "refresh_token",
            ["refresh_token"] = _refreshToken,
            ["client_id"] = "your-client-id",
            // 按需添加其他参数,比如client_secret
        })
    };

    var response = await client.SendAsync(tokenRequest, cancellationToken);
    if (!response.IsSuccessStatusCode)
    {
        // 刷新失败,返回null触发后续登录逻辑
        return null;
    }

    return await response.Content.ReadFromJsonAsync<Tokens>(cancellationToken);
}

// 令牌数据模型
private class Tokens
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
}

关键细节说明

  • 为什么用SemaphoreSlim而非lock?:lock是同步锁,在异步方法中会阻塞线程;SemaphoreSlim支持WaitAsync,可以异步等待,不会浪费线程资源。
  • 双重检查令牌:拿到锁后再次校验令牌,避免等待期间已有其他请求完成刷新,减少不必要的重复操作。
  • 请求克隆:必须克隆请求重试,否则原请求的Content流已被读取,会导致发送失败。

这样修改后,不管多少并发请求触发401,只会有一个请求执行刷新逻辑,其他请求等待锁释放后直接用新令牌重试,彻底解决重复刷新的问题。

内容的提问来源于stack exchange,提问作者Humpy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 07:12:28