.NET Core 2.2下基于IHttpClientFactory的Token刷新优雅实现方案咨询
优雅实现基于IHttpClientFactory的令牌刷新与配置更新方案
针对你的需求,我推荐把令牌认证、刷新逻辑抽离到自定义HTTP消息处理器中,配合命名HttpClient实现业务代码与认证逻辑的解耦,同时解决appsettings.json的持久化更新问题。下面是完整的实现方案:
核心思路
- 用DelegatingHandler封装认证逻辑:所有需要令牌的请求自动带上Authorization头,遇到401/403时自动触发令牌刷新、重试请求,无需在业务方法中重复写判断逻辑。
- 线程安全的令牌刷新:用信号量防止多请求同时触发刷新,避免重复调用令牌接口。
- 持久化更新appsettings.json:直接修改配置文件并重新加载,让后续请求自动读取新令牌。
- 命名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>(); } }
关键优势说明
- 解耦认证与业务:认证逻辑完全封装在Handler中,业务代码无需关心令牌刷新细节。
- 线程安全:用
SemaphoreSlim确保同一时间只有一个请求触发令牌刷新,避免重复调用刷新接口。 - 持久化配置更新:直接修改appsettings.json并重新加载,重启服务后新令牌依然有效。
- 灵活扩展:可以轻松添加重试次数限制、日志记录等扩展逻辑到Handler中。
内容的提问来源于stack exchange,提问作者D.B
相关产品推荐
相关产品推荐

