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

如何处理多并行请求下的JWT与刷新令牌问题

解决方案:处理多并行请求下的令牌刷新冲突

你的问题核心是多并行请求同时触发令牌刷新,导致旧refresh-token被重复使用而失效。在ASP.NET Core 6.0中,可以通过以下方式解决:

核心思路

确保同一用户的令牌刷新操作是串行执行的——同一时间只有一个请求能执行刷新逻辑,其他请求等待刷新完成后直接复用新令牌,避免重复调用刷新接口导致的refresh-token失效。

具体实现步骤

1. 实现用户级锁管理器

创建一个全局锁管理器,按用户ID隔离锁,确保同一用户的刷新操作互斥:

public interface IUserLockManager
{
    Task<IDisposable> AcquireLockAsync(string userId);
}

public class UserLockManager : IUserLockManager
{
    private readonly ConcurrentDictionary<string, SemaphoreSlim> _userLocks = new();

    public async Task<IDisposable> AcquireLockAsync(string userId)
    {
        // 为每个用户创建或获取对应的信号量
        var semaphore = _userLocks.GetOrAdd(userId, _ => new SemaphoreSlim(1, 1));
        await semaphore.WaitAsync();
        // 返回自动释放锁的对象
        return new SemaphoreReleaser(semaphore);
    }

    private class SemaphoreReleaser : IDisposable
    {
        private readonly SemaphoreSlim _semaphore;

        public SemaphoreReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore;

        public void Dispose() => _semaphore.Release();
    }
}

2. 注册服务

在Program.cs中注册锁管理器为单例(全局共享),同时确保IHttpContextAccessor和IHttpClientFactory可用:

builder.Services.AddSingleton<IUserLockManager, UserLockManager>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient("ApiClient"); // 用HttpClientFactory管理HttpClient
builder.Services.AddScoped<IApiCaller, MyApiCaller>();

3. 修改MyApiCaller类

调整令牌获取逻辑,引入锁机制避免重复刷新:

public class MyApiCaller : IApiCaller
{
    private readonly HttpClient _apiClient;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IUserLockManager _userLockManager;
    private readonly IConfiguration _configuration;

    public bool IsLoggedIn => GetCurrentToken() != null;
    public Uri BaseUri => _apiClient.BaseAddress;

    public MyApiCaller(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor, 
                       IUserLockManager userLockManager, IConfiguration configuration)
    {
        _apiClient = httpClientFactory.CreateClient("ApiClient");
        _apiClient.BaseAddress = new Uri(configuration.GetValue<string>("ApiRootUrl"));
        _apiClient.DefaultRequestHeaders.Clear();
        _apiClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        
        _httpContextAccessor = httpContextAccessor;
        _userLockManager = userLockManager;
        _configuration = configuration;
    }

    // 从Cookie实时获取当前JWT令牌
    private string GetCurrentToken()
    {
        return _httpContextAccessor.HttpContext?.Request.GetTokenFromCookie(); // 替换为你的Cookie读取逻辑
    }

    // 从Cookie实时获取当前刷新令牌
    private string GetCurrentRefreshToken()
    {
        return _httpContextAccessor.HttpContext?.Request.GetRefreshTokenFromCookie(); // 替换为你的Cookie读取逻辑
    }

    public void SetCredentials(string token, string refreshToken)
    {
        _apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        _httpContextAccessor.HttpContext.Response.UpdateTokensInCookie(token, refreshToken); // 你的Cookie更新逻辑
    }

    public void Logout()
    {
        _apiClient.DefaultRequestHeaders.Authorization = null;
        _httpContextAccessor.HttpContext.Response.ClearAuthCookie(); // 你的登出Cookie清理逻辑
    }

    public async Task<HttpResponseMessage> GetAsync(Uri uri)
    {
        // 每次请求前同步最新令牌到HttpClient
        var currentToken = GetCurrentToken();
        if (currentToken != null)
        {
            _apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", currentToken);
        }

        var response = await _apiClient.GetAsync(uri);

        if (response.IsSuccessStatusCode)
            return response;

        // 处理令牌过期情况
        if (response.StatusCode == HttpStatusCode.Unauthorized && 
            response.Headers.Contains(CustomResponseHeaders.ExpiredTokenHeader))
        {
            var userId = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
            if (string.IsNullOrEmpty(userId))
                return response;

            // 获取用户锁,串行化刷新操作
            using (await _userLockManager.AcquireLockAsync(userId))
            {
                // 双重检查:确认等待期间令牌是否已被其他请求刷新
                var newToken = GetCurrentToken();
                if (newToken != currentToken)
                {
                    // 令牌已刷新,直接用新令牌重试
                    _apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
                    return await GetAsync(uri);
                }

                // 执行刷新操作
                bool refreshed = await RefreshTokenAsync();
                if (!refreshed)
                    return response;

                // 用新令牌重试请求
                return await GetAsync(uri);
            }
        }

        return response;
    }

    private async Task<bool> RefreshTokenAsync()
    {
        var token = GetCurrentToken();
        var refreshToken = GetCurrentRefreshToken();
        if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refreshToken))
            return false;

        var requestBody = JsonConvert.SerializeObject(new RefreshTokenRequest
        {
            Token = token,
            RefreshToken = refreshToken
        });
        var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
        
        var response = await _apiClient.PostAsync("identity/refresh-token", content);
        if (response.IsSuccessStatusCode)
        {
            var authResponse = await response.ToModelAsync<SuccessfulAuthResponse>();
            SetCredentials(authResponse.Token, authResponse.RefreshToken);
            return true;
        }
        else
        {
            // 刷新失败,执行登出逻辑
            Logout();
            return false;
        }
    }
}

关键优化点说明

  • 用户级锁:通过ConcurrentDictionary+SemaphoreSlim实现用户维度的互斥锁,避免全局锁影响不同用户的请求。
  • 实时令牌同步:每次请求前从Cookie读取最新令牌,确保HttpClient使用的是当前有效的令牌,避免实例缓存的令牌与Cookie不一致。
  • 双重检查机制:获取锁后再次验证令牌状态,避免等待期间其他请求已完成刷新,重复执行无用的刷新操作。
  • HttpClientFactory:使用官方推荐的IHttpClientFactory管理HttpClient,避免手动创建导致的连接泄漏问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 17:05:32