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

外部服务返回Unauthorised时后台重认证的最优方案与中间件可行性咨询

最佳实现方案及中间件可行性分析

核心结论

最佳方案是基于HttpClient的DelegatingHandler封装自动令牌刷新+重试逻辑,专门处理我方服务调用外部服务的出站请求。普通ASP.NET Core中间件并非最优选择,因为它的定位是处理用户发起的入站请求,而非服务间的出站调用。


为什么选DelegatingHandler?

DelegatingHandler是HttpClient的扩展点,能直接拦截出站请求的发送和响应处理,完美契合“调用外部服务时自动处理401”的需求:

  • 逻辑聚焦在HTTP请求链路,与业务代码解耦
  • 所有使用该HttpClient的请求都会自动应用认证逻辑,复用性强
  • 避免在每个业务方法里重复写401判断和重试代码

具体实现步骤

1. 实现自定义认证Handler

这个Handler会负责:

  • 自动携带缓存的令牌
  • 收到401时刷新令牌并重试请求
  • 保证线程安全,避免并发刷新令牌
public class AuthenticationDelegatingHandler : DelegatingHandler
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;
    private readonly AuthenticationSettings _authSettings;
    private readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1);

    public AuthenticationDelegatingHandler(
        IHttpClientFactory httpClientFactory,
        IMemoryCache cache,
        IOptions<AuthenticationSettings> authSettings)
    {
        _httpClientFactory = httpClientFactory;
        _cache = cache;
        _authSettings = authSettings.Value;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        // 从缓存获取令牌,自动添加到请求头
        var token = await GetValidTokenAsync(cancellationToken);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

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

        // 遇到401时,刷新令牌并重试一次
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            await _refreshLock.WaitAsync(cancellationToken);
            try
            {
                // 双重检查缓存,避免其他线程已刷新令牌
                var newToken = await GetValidTokenAsync(cancellationToken, forceRefresh: true);
                if (newToken != token)
                {
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
                    response = await base.SendAsync(request, cancellationToken);
                }
            }
            finally
            {
                _refreshLock.Release();
            }
        }

        return response;
    }

    private async Task<string> GetValidTokenAsync(
        CancellationToken cancellationToken, 
        bool forceRefresh = false)
    {
        const string cacheKey = "ExternalService_AuthToken";

        // 未强制刷新时优先用缓存
        if (!forceRefresh && _cache.TryGetValue(cacheKey, out string cachedToken))
        {
            return cachedToken;
        }

        await _refreshLock.WaitAsync(cancellationToken);
        try
        {
            // 再次检查缓存,防止并发场景下重复刷新
            if (!forceRefresh && _cache.TryGetValue(cacheKey, out cachedToken))
            {
                return cachedToken;
            }

            // 调用外部认证接口获取新令牌
            var authClient = _httpClientFactory.CreateClient();
            var authRequest = new HttpRequestMessage(HttpMethod.Post, _authSettings.AuthenticateURL)
            {
                Headers = { { HeaderNames.Authorization, _authSettings.Token } }
            };

            var authResponse = await authClient.SendAsync(authRequest, cancellationToken);
            authResponse.EnsureSuccessStatusCode(); // 认证失败直接抛出异常

            var tokenModel = await authResponse.Content.ReadFromJsonAsync<AuthToken>(cancellationToken);
            if (tokenModel == null)
            {
                throw new InvalidOperationException("未能获取有效的认证令牌");
            }

            // 缓存令牌,提前5分钟过期避免请求时刚好失效
            var cacheOptions = new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(tokenModel.ExpiresIn - 5));
            _cache.Set(cacheKey, tokenModel.AccessToken, cacheOptions);

            return tokenModel.AccessToken;
        }
        finally
        {
            _refreshLock.Release();
        }
    }
}

2. 注册Handler和HttpClient

在Program.cs中配置DI,将自定义Handler绑定到特定的HttpClient:

// 注册认证Handler
services.AddScoped<AuthenticationDelegatingHandler>();

// 配置外部服务的HttpClient,自动应用认证Handler
services.AddHttpClient("ExternalServiceClient")
    .AddHttpMessageHandler<AuthenticationDelegatingHandler>();

// 绑定认证配置
services.Configure<AuthenticationSettings>(configuration.GetSection("AuthenticationSettings"));

3. 修改业务代码使用配置好的HttpClient

现在业务方法无需再处理认证逻辑,直接用配置好的HttpClient即可:

private readonly HttpClient _externalServiceClient;
private readonly AppSettings _appSettings;

public NotificationService(
    IHttpClientFactory httpClientFactory,
    IOptions<AppSettings> appSettings)
{
    _externalServiceClient = httpClientFactory.CreateClient("ExternalServiceClient");
    _appSettings = appSettings.Value;
}

public async Task FetchNotifications(string projectId)
{
    try
    {
        string notificationUrl = $"{_appSettings.BaseURL}/notifications";
        var response = await _externalServiceClient.GetAsync(notificationUrl);
        response.EnsureSuccessStatusCode(); // 若刷新令牌后仍401,直接抛出异常

        // 处理返回的通知数据
        var notifications = await response.Content.ReadFromJsonAsync<List<Notification>>();
        // ...后续业务逻辑
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}

关键细节说明

  • 线程安全:用SemaphoreSlim控制令牌刷新的并发,避免多个请求同时调用认证接口
  • 缓存策略:提前5分钟过期令牌,避免请求时刚好遇到令牌失效的情况
  • 重试限制:只重试一次,防止无限循环(若刷新令牌后仍401,说明认证逻辑存在问题,需抛出异常让上层处理)

中间件是否可行?

普通ASP.NET Core中间件主要用于处理用户发起的入站请求(比如用户调用我方API的请求),如果用它来处理外部服务的出站请求,需要在中间件里捕获所有可能调用外部服务的场景,逻辑会非常零散,耦合度极高,维护成本也大。因此不推荐用普通中间件实现这个需求。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 22:48:52