使用IHttpClientFactory调用外部API时,Access/Refresh令牌存储与更新方案问询
关于.NET Core 2.2中令牌存储与刷新的解决方案
嘿,我来帮你梳理下这个问题——之前在做外部API集成的时候,我也踩过直接改appsettings.json的坑,正好分享下实用的解决思路:
一、为什么不能直接更新appsettings.json?
首先得明确:appsettings.json是只读的配置源。.NET Core启动时会把所有配置加载到内存中的IConfiguration对象里,后续修改json文件并不会自动同步到应用的配置实例中。而且直接写入文件还会带来这些问题:
- 并发写入时可能出现文件锁或数据损坏;
- 生产环境中修改配置文件可能需要重启应用(如果没配置热重载);
- 权限问题:应用进程可能没有写入配置文件的权限。
那该怎么处理令牌的动态更新?答案是把令牌存在内存缓存里,而不是配置文件中。
实现思路:封装令牌管理类
你可以创建一个专门的TokenManager类,负责令牌的获取、刷新和缓存,结合IMemoryCache来实现:
public class TokenManager { private readonly IMemoryCache _cache; private readonly HttpClient _httpClient; private readonly IConfiguration _config; private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); public TokenManager(IMemoryCache cache, HttpClient httpClient, IConfiguration config) { _cache = cache; _httpClient = httpClient; _config = config; } public async Task<string> GetValidAccessTokenAsync() { // 先从缓存取令牌 if (_cache.TryGetValue("ValidAccessToken", out string token)) { return token; } await _semaphore.WaitAsync(); try { // 双重检查,避免并发请求重复刷新 if (_cache.TryGetValue("ValidAccessToken", out token)) { return token; } // 这里调用刷新令牌的API,获取新的Access Token和Refresh Token var refreshToken = _config["Tokens:RefreshToken"]; // 初始Refresh Token可以从配置读取 var newTokens = await RefreshTokensAsync(refreshToken); // 将新令牌存入缓存,设置过期时间(比实际令牌过期时间早5分钟,提前刷新) var cacheEntryOptions = new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromMinutes(newTokens.ExpiresIn - 5)); _cache.Set("ValidAccessToken", newTokens.AccessToken, cacheEntryOptions); _cache.Set("ValidRefreshToken", newTokens.RefreshToken); // 也缓存Refresh Token return newTokens.AccessToken; } finally { _semaphore.Release(); } } private async Task<TokensResponse> RefreshTokensAsync(string refreshToken) { // 调用外部API的刷新令牌接口,这里是示例逻辑 var response = await _httpClient.PostAsync("/token/refresh", new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "refresh_token", ["refresh_token"] = refreshToken })); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsAsync<TokensResponse>(); } // 清空缓存的方法,用于401重试时触发 public void ClearTokenCache() { _cache.Remove("ValidAccessToken"); _cache.Remove("ValidRefreshToken"); } } // 令牌响应模型 public class TokensResponse { public string AccessToken { get; set; } public string RefreshToken { get; set; } public int ExpiresIn { get; set; } }
然后在使用HttpClient发送请求时,先从TokenManager获取有效令牌:
public class ExternalApiClient { private readonly HttpClient _httpClient; private readonly TokenManager _tokenManager; public ExternalApiClient(HttpClient httpClient, TokenManager tokenManager) { _httpClient = httpClient; _tokenManager = tokenManager; } public async Task<ApiResponse> GetDataAsync() { var token = await _tokenManager.GetValidAccessTokenAsync(); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.GetAsync("/api/data"); // 处理401/403:如果令牌过期,清空缓存并重试一次 if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden) { _tokenManager.ClearTokenCache(); token = await _tokenManager.GetValidAccessTokenAsync(); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); response = await _httpClient.GetAsync("/api/data"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadAsAsync<ApiResponse>(); } }
二、更优的令牌存储方案
除了内存缓存,根据你的应用场景,还有这些更好的选择:
1. 分布式缓存(多实例/分布式应用)
如果你的应用是多实例部署,或者用了微服务,单实例的内存缓存就不够用了——每个实例会各自刷新令牌,造成资源浪费。这时候可以用Redis分布式缓存,通过IDistributedCache来存储令牌,所有实例共享同一个令牌缓存。
2. 配置中心(后端服务场景)
如果是没有用户上下文的后台服务、定时任务,你可以把初始令牌存在配置中心(比如Consul、Azure App Configuration、Nacos),刷新令牌后,把新的令牌更新到配置中心,这样所有应用实例都能自动获取最新的令牌(配置中心一般支持配置热重载)。
3. 安全存储(涉及用户令牌的场景)
如果是用户级别的令牌(比如每个用户有自己的Access/Refresh Token),那不能存在缓存里,应该用安全存储:
- 后端可以存在数据库(加密存储);
- 如果是前端+后端的场景,Refresh Token可以存在HttpOnly的Cookie中,Access Token存在前端内存里。
总结
- 不要直接修改appsettings.json,用内存缓存或分布式缓存来动态管理令牌;
- 封装令牌管理类,处理并发刷新和过期逻辑;
- 根据应用的部署架构和场景选择合适的存储方案。
内容的提问来源于stack exchange,提问作者D.B
相关产品推荐
相关产品推荐

