使用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
相关产品推荐
相关产品推荐

