ASP.NET Core异步请求时Access Token获取冲突的解决方案咨询
问题分析与解决方案
问题根源
- 并发场景下,多个请求同时触发token校验逻辑,重复调用第三方API的token接口,引发限流或报错。
async void类型的ValidateExampleToken方法无法被等待,加剧了竞态条件;共享的_authRespModel和HttpClient.DefaultRequestHeaders在无同步保护的情况下被并发修改,导致状态不一致。- 构造函数中用
.Result同步调用异步方法,存在死锁风险。
方案一:用自定义DelegatingHandler实现自动token管理(推荐)
这种方式相当于给HttpClient添加"中间件",将token管理逻辑与业务代码解耦,天然支持并发场景。
1. 创建Token认证消息处理程序
public class ExampleTokenHandler : DelegatingHandler { private readonly IOptions<ExampleClientSettings> _config; private ExampleAuthenticationResponseModel? _currentToken; private readonly SemaphoreSlim _tokenLock = new SemaphoreSlim(1, 1); public ExampleTokenHandler(IOptions<ExampleClientSettings> config) { _config = config; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 获取有效token var token = await GetValidTokenAsync(cancellationToken); // 为请求添加Authorization头 request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(token.Token_Type, token.Access_Token); // 发送请求 var response = await base.SendAsync(request, cancellationToken); // 遇到401时刷新token并重试一次 if (response.StatusCode == HttpStatusCode.Unauthorized) { await _tokenLock.WaitAsync(cancellationToken); try { // 双重检查避免重复刷新 if (_currentToken?.Expires > DateTime.Now) { token = _currentToken; } else { token = await FetchNewTokenAsync(cancellationToken); _currentToken = token; } } finally { _tokenLock.Release(); } // 更新请求头并重试 request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(token.Token_Type, token.Access_Token); response = await base.SendAsync(request, cancellationToken); } return response; } private async Task<ExampleAuthenticationResponseModel> GetValidTokenAsync(CancellationToken cancellationToken) { await _tokenLock.WaitAsync(cancellationToken); try { if (_currentToken == null || _currentToken.Expires <= DateTime.Now) { _currentToken = await FetchNewTokenAsync(cancellationToken); } return _currentToken; } finally { _tokenLock.Release(); } } private async Task<ExampleAuthenticationResponseModel> FetchNewTokenAsync(CancellationToken cancellationToken) { // 创建临时HttpClient获取token,避免循环调用自身Handler using var client = new HttpClient(); var body = $"grant_type=password&username={Uri.EscapeDataString(_config.Value.UserName)}&password={Uri.EscapeDataString(_config.Value.Password)}&client_id=ngAuthApp"; var content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"); var response = await client.PostAsync("https://api.nowcerts.com/api/token", content, cancellationToken); response.EnsureSuccessStatusCode(); var contentString = await response.Content.ReadAsStringAsync(cancellationToken); var tokenModel = JsonSerializer.Deserialize<ExampleAuthenticationResponseModel>(contentString) ?? throw new Exception("无法反序列化token响应"); return tokenModel; } }
2. 在DI容器中注册Handler和HttpClient
// Program.cs builder.Services.AddTransient<ExampleTokenHandler>(); builder.Services.AddHttpClient<IExampleService, ExampleService>(client => { client.BaseAddress = new Uri("https://api.nowcerts.com/"); }) .AddHttpMessageHandler<ExampleTokenHandler>();
3. 简化ExampleService代码
去掉原有token管理逻辑,专注业务请求:
public class ExampleService : IExampleService { private readonly ILogger<ExampleService> _logger; private readonly HttpClient _exampleClient; private readonly int _batchSize; public ExampleService(ILogger<ExampleService> logger, HttpClient client, IOptions<ExampleClientSettings> configuration) { _logger = logger; _exampleClient = client; _batchSize = configuration.Value.BatchResultAmount ?? 0; } public async Task<ExampleInsuredModel?> GetInsured(Guid exampleId) { try { HttpResponseMessage response = await _exampleClient.GetAsync($"api/InsuredDetailList?$count=true&$filter=id eq {exampleId}"); response.EnsureSuccessStatusCode(); string content = await response.Content.ReadAsStringAsync(); ExampleInsuredListResponseModel model = JsonSerializer.Deserialize<ExampleInsuredListResponseModel>(content) ?? new ExampleInsuredListResponseModel(); if (model.Count > 1 || model.Value.Count > 1) { throw new Exception($"Multiple insured accounts found with example id {exampleId}"); } else if (model.Count == 1 && model.Value.Count == 1) { return model.Value[0]; } else { return null; } } catch { throw; } } }
方案二:修复原有代码的线程安全问题
如果不想引入新的Handler组件,可以通过同步锁修复现有逻辑:
修改后的ExampleService代码
public class ExampleService : IExampleService { private readonly ILogger<ExampleService> _logger; private readonly HttpClient _exampleClient; private ExampleAuthenticationResponseModel? _authRespModel; private readonly string _exampleUserName; private readonly string _examplePassword; private readonly int _batchSize; private readonly SemaphoreSlim _tokenLock = new SemaphoreSlim(1, 1); public ExampleService(ILogger<ExampleService> logger, HttpClient client, IOptions<ExampleClientSettings> configuration) { _logger = logger; _exampleClient = client; _exampleUserName = configuration.Value.UserName ?? throw new ArgumentNullException(nameof(configuration.Value.UserName)); _examplePassword = configuration.Value.Password ?? throw new ArgumentNullException(nameof(configuration.Value.Password)); _batchSize = configuration.Value.BatchResultAmount ?? 0; } private async Task ValidateExampleTokenAsync() { await _tokenLock.WaitAsync(); try { // 双重检查锁,避免已刷新token后重复操作 if (_authRespModel == null || _authRespModel.Expires <= DateTime.Now) { _authRespModel = await GetExampleToken(); _exampleClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_authRespModel.Token_Type, _authRespModel.Access_Token); } } finally { _tokenLock.Release(); } } private async Task<ExampleAuthenticationResponseModel> GetExampleToken() { var body = $"grant_type=password&username={Uri.EscapeDataString(_exampleUserName)}&password={Uri.EscapeDataString(_examplePassword)}&client_id=ngAuthApp"; StringContent content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"); var response = await _exampleClient.PostAsync("api/token", content); if (!response.IsSuccessStatusCode) { throw new HttpRequestException("Unable to authenticate example credentials"); } var contentString = await response.Content.ReadAsStringAsync(); if (contentString == null) { throw new HttpRequestException("Example authentication response is Null."); } var respModel = JsonSerializer.Deserialize<ExampleAuthenticationResponseModel>(contentString); if (respModel == null) throw new Exception("Unable to deserialize example authentication response."); return respModel; } public async Task<ExampleInsuredModel?> GetInsured(Guid exampleId) { try { await ValidateExampleTokenAsync(); // 改为异步等待 HttpResponseMessage response = await _exampleClient.GetAsync($"api/InsuredDetailList?$count=true&$filter=id eq {exampleId}"); response.EnsureSuccessStatusCode(); string content = await response.Content.ReadAsStringAsync(); ExampleInsuredListResponseModel model = JsonSerializer.Deserialize<ExampleInsuredListResponseModel>(content) ?? new ExampleInsuredListResponseModel(); if (model.Count > 1 || model.Value.Count > 1) { throw new Exception($"Multiple insured accounts found with example id {exampleId}"); } else if (model.Count == 1 && model.Value.Count == 1) { return model.Value[0]; } else { return null; } } catch { throw; } } }
关键修复点
- 将
async void改为async Task,确保异步操作被等待 - 使用
SemaphoreSlim实现异步锁,保证同一时间只有一个请求刷新token - 对用户名密码进行URL编码,避免特殊字符导致的请求错误
- 移除构造函数中同步调用异步方法的逻辑,延迟首次token获取到第一个请求
内容的提问来源于stack exchange,提问作者Jon Sowers
相关产品推荐
相关产品推荐

