HttpClient单例复用场景下Refresh Token更新DefaultRequestHeaders的实现问询
嘿,这个问题刚好戳中了HttpClient复用场景下的一个常见痛点——既要遵循微软“单实例复用”的最佳实践,又要安全处理令牌过期刷新的问题。先给你把核心矛盾点说透:DefaultRequestHeaders确实不是线程安全的,并发场景下直接修改(清空再添加新令牌)会导致竞态条件,比如有的请求可能拿到半截的头部,或者新旧令牌混用,完全不可靠。
下面给你推荐几种靠谱的解决方案,按实用性排序:
1. 用DelegatingHandler拦截请求动态添加令牌(首推)
这是最安全、最符合设计理念的方式——不要碰全局的DefaultRequestHeaders,而是给HttpClient加一个自定义的请求拦截器,在每个请求发送前单独处理令牌逻辑:
public class AuthenticationHandler : DelegatingHandler { private readonly ITokenService _tokenService; public AuthenticationHandler(ITokenService tokenService) { _tokenService = tokenService; } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { // 先检查令牌是否过期,需要的话刷新 var token = await _tokenService.GetValidTokenAsync(cancellationToken); // 给当前请求单独添加Authorization头部,不会影响全局 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); // 继续发送请求 var response = await base.SendAsync(request, cancellationToken); // 可选:如果返回401,刷新令牌并重试一次(这里可以结合Polly做更优雅的重试) if (response.StatusCode == HttpStatusCode.Unauthorized) { token = await _tokenService.RefreshTokenAsync(cancellationToken); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); response = await base.SendAsync(request, cancellationToken); } return response; } }
然后在注册HttpClient的时候把这个Handler加进去(以.NET Core/.NET 5+为例):
services.AddHttpClient("MyApiClient") .AddHttpMessageHandler<AuthenticationHandler>();
这种方式的好处是:每个请求的头部都是独立处理的,完全不会有线程安全问题,而且令牌逻辑和请求发送解耦,维护起来也方便。
2. 加锁保护DefaultRequestHeaders修改(不推荐,但应急可用)
如果实在不想改架构,非要用全局的DefaultRequestHeaders,那必须用锁来强制串行化修改操作,避免并发冲突:
private readonly HttpClient _httpClient; private readonly object _headerLock = new object(); private readonly ITokenService _tokenService; public MyService(HttpClient httpClient, ITokenService tokenService) { _httpClient = httpClient; _tokenService = tokenService; } public async Task RefreshAndUpdateTokenAsync() { var newToken = await _tokenService.RefreshTokenAsync(); lock (_headerLock) { _httpClient.DefaultRequestHeaders.Remove("Authorization"); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {newToken}"); } }
但要注意:这种方式有明显弊端——锁会阻塞所有请求,要是令牌刷新操作耗时久,会导致请求排队;而且修改后,在修改前已经发起的请求还是会用旧令牌,可能出现401。所以只适合低并发、令牌刷新极快的场景。
3. 结合IHttpClientFactory和Polly策略(现代.NET最佳实践)
如果用的是.NET Core/.NET 5+,推荐直接用IHttpClientFactory结合Polly库来处理令牌刷新和重试,这套方案更完整:
- 注册Polly重试策略,当遇到401时自动刷新令牌并重试;
- 把令牌逻辑封装在策略里,完全不用碰DefaultRequestHeaders。
示例代码大概是这样:
services.AddHttpClient("MyApiClient") .AddPolicyHandler((services, request) => Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.Unauthorized) .RetryAsync(1, async (response, retryCount, context) => { var tokenService = services.GetRequiredService<ITokenService>(); var newToken = await tokenService.RefreshTokenAsync(); // 更新当前请求的头部 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken); }));
这种方式既遵循了HttpClient复用的最佳实践,又安全处理了令牌刷新,还自带重试机制,非常适合生产环境。
总结一下:永远不要在并发场景下直接修改DefaultRequestHeaders,首选DelegatingHandler或者IHttpClientFactory+Polly的组合,这才是安全可靠的方案。
内容的提问来源于stack exchange,提问作者Philippe Lécaillon

