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

ASP.NET Core异步请求时Access Token获取冲突的解决方案咨询

问题分析与解决方案

问题根源

  1. 并发场景下,多个请求同时触发token校验逻辑,重复调用第三方API的token接口,引发限流或报错。
  2. async void类型的ValidateExampleToken方法无法被等待,加剧了竞态条件;共享的_authRespModel和HttpClient.DefaultRequestHeaders在无同步保护的情况下被并发修改,导致状态不一致。
  3. 构造函数中用.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 22:02:33