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

.NET Core 2.2下基于IHttpClientFactory的Token刷新优雅实现方案咨询

优雅实现基于IHttpClientFactory的令牌刷新与配置更新方案

针对你的需求,我推荐把令牌认证、刷新逻辑抽离到自定义HTTP消息处理器中,配合命名HttpClient实现业务代码与认证逻辑的解耦,同时解决appsettings.json的持久化更新问题。下面是完整的实现方案:

核心思路

  1. 用DelegatingHandler封装认证逻辑:所有需要令牌的请求自动带上Authorization头,遇到401/403时自动触发令牌刷新、重试请求,无需在业务方法中重复写判断逻辑。
  2. 线程安全的令牌刷新:用信号量防止多请求同时触发刷新,避免重复调用令牌接口。
  3. 持久化更新appsettings.json:直接修改配置文件并重新加载,让后续请求自动读取新令牌。
  4. 命名HttpClient区分场景:为需要认证的API和普通API分别注册独立的客户端,互不干扰。

步骤1:定义配置模型与令牌响应模型

先把配置和令牌响应结构化:

// appsettings.json对应的配置模型
public class AppSettings
{
    public string Token { get; set; }
    public string RefreshToken { get; set; }
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
    public string TokenEndpoint { get; set; }
    public string ProtectedApiBaseUrl { get; set; }
}

// 令牌刷新接口的响应模型
public class RefreshTokenResponse
{
    public string access_token { get; set; }
    public string refresh_token { get; set; } // 通常刷新后会返回新的刷新令牌
    public int expires_in { get; set; }
}

步骤2:实现自定义认证消息处理器

这个处理器会自动处理令牌添加、刷新和请求重试:

public class AuthenticationHandler : DelegatingHandler
{
    private readonly IOptionsMonitor<AppSettings> _appSettings;
    private readonly IConfigurationRoot _configurationRoot;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1);

    public AuthenticationHandler(IOptionsMonitor<AppSettings> appSettings,
                                 IConfigurationRoot configurationRoot,
                                 IHttpClientFactory httpClientFactory)
    {
        _appSettings = appSettings;
        _configurationRoot = configurationRoot;
        _httpClientFactory = httpClientFactory;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // 1. 为请求添加当前令牌
        var currentToken = _appSettings.CurrentValue.Token;
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", currentToken);

        // 2. 发送请求
        var response = await base.SendAsync(request, cancellationToken);

        // 3. 遇到401/403时尝试刷新令牌并重试
        if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
        {
            await _refreshLock.WaitAsync(cancellationToken);
            try
            {
                // 双重检查:防止其他请求已经刷新了令牌
                var latestToken = _appSettings.CurrentValue.Token;
                if (latestToken != currentToken)
                {
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", latestToken);
                    return await base.SendAsync(request, cancellationToken);
                }

                // 调用刷新令牌接口
                var newTokenData = await RefreshTokenAsync(cancellationToken);
                if (newTokenData == null) return response;

                // 更新配置文件与内存配置
                await UpdateAppSettingsAsync(newTokenData);

                // 用新令牌重试请求
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newTokenData.access_token);
                response = await base.SendAsync(request, cancellationToken);
            }
            finally
            {
                _refreshLock.Release();
            }
        }

        return response;
    }

    // 调用令牌刷新接口
    private async Task<RefreshTokenResponse> RefreshTokenAsync(CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient();
        var formData = new List<KeyValuePair<string, string>>
        {
            new("client_id", _appSettings.CurrentValue.ClientId),
            new("client_secret", _appSettings.CurrentValue.ClientSecret),
            new("grant_type", "refresh_token"),
            new("refresh_token", _appSettings.CurrentValue.RefreshToken)
        };

        var request = new HttpRequestMessage(HttpMethod.Post, _appSettings.CurrentValue.TokenEndpoint)
        {
            Content = new FormUrlEncodedContent(formData)
        };

        var response = await client.SendAsync(request, cancellationToken);
        return response.IsSuccessStatusCode 
            ? await response.Content.ReadAsAsync<RefreshTokenResponse>(cancellationToken) 
            : null;
    }

    // 更新appsettings.json并重新加载配置
    private async Task UpdateAppSettingsAsync(RefreshTokenResponse tokenData)
    {
        var appSettingsPath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
        var jsonContent = await File.ReadAllTextAsync(appSettingsPath);
        var jsonObj = JObject.Parse(jsonContent);

        // 更新配置字段
        jsonObj["AppSettings"]["Token"] = tokenData.access_token;
        jsonObj["AppSettings"]["RefreshToken"] = tokenData.refresh_token;

        // 写回文件
        await File.WriteAllTextAsync(appSettingsPath, jsonObj.ToString(Formatting.Indented));

        // 触发配置重新加载,让IOptionsMonitor感知到变化
        _configurationRoot.Reload();
    }
}

步骤3:在Startup中注册服务与HttpClient

区分需要认证和不需要认证的客户端,配置好依赖:

public void ConfigureServices(IServiceCollection services)
{
    // 配置AppSettings,启用文件变更自动重载
    services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

    // 注册IConfigurationRoot用于重新加载配置
    services.AddSingleton<IConfigurationRoot>(provider => (IConfigurationRoot)Configuration);

    // 注册自定义认证处理器
    services.AddTransient<AuthenticationHandler>();

    // 1. 注册需要令牌认证的命名HttpClient
    services.AddHttpClient("ProtectedApiClient")
        .AddHttpMessageHandler<AuthenticationHandler>()
        .ConfigureHttpClient(c =>
        {
            c.BaseAddress = new Uri(Configuration["AppSettings:ProtectedApiBaseUrl"]);
        });

    // 2. 注册不需要认证的普通HttpClient
    services.AddHttpClient("PublicApiClient");

    // 其他服务注册...
}

步骤4:在业务代码中使用HttpClient

现在业务方法可以完全摆脱认证逻辑,专注于业务:

public class YourBusinessService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public YourBusinessService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<AirCallRequest> GetInformationAsync(AirCallModel model)
    {
        try
        {
            var client = _httpClientFactory.CreateClient("ProtectedApiClient");
            var response = await client.GetAsync("v2/*****"); // 自动拼接BaseAddress

            if (response.IsSuccessStatusCode)
            {
                var candidateResult = await response.Content.ReadAsAsync<CandidateResults>();
                // 处理业务逻辑...
                return /* 你的返回值 */;
            }

            return null;
        }
        catch (Exception ex)
        {
            // 日志记录ex
            return null;
        }
    }

    // 调用不需要认证的API方法示例
    public async Task<AnotherResponse> CallPublicApiAsync()
    {
        var client = _httpClientFactory.CreateClient("PublicApiClient");
        var response = await client.GetAsync("https://public-api-url.com/endpoint");
        return await response.Content.ReadAsAsync<AnotherResponse>();
    }
}

关键优势说明

  1. 解耦认证与业务:认证逻辑完全封装在Handler中,业务代码无需关心令牌刷新细节。
  2. 线程安全:用SemaphoreSlim确保同一时间只有一个请求触发令牌刷新,避免重复调用刷新接口。
  3. 持久化配置更新:直接修改appsettings.json并重新加载,重启服务后新令牌依然有效。
  4. 灵活扩展:可以轻松添加重试次数限制、日志记录等扩展逻辑到Handler中。

内容的提问来源于stack exchange,提问作者D.B

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 07:56:27