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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 07:40:48