如何处理多并行请求下的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
相关产品推荐
相关产品推荐

