使用Refresh Token刷新Bearer Token时HttpClient请求异常问题
解决Token刷新时的TaskCancelletionException问题及并发请求处理
你的问题核心出在两个地方:一是httpClient.CancelPendingRequests()会取消所有挂起请求(包括你刚要发起的刷新Token请求),直接导致了取消异常;二是没有处理并发场景下多个请求同时触发Token刷新的冲突。下面是具体的解决方案:
1. 核心修复思路
- 移除全局取消请求的操作,避免刷新请求被误取消
- 用异步锁保证同一时间只有一个刷新Token的操作在执行
- 优化重试逻辑,确保刷新成功后用新Token正确重试原请求
2. 具体代码实现
第一步:添加异步锁字段
在你的请求处理类中添加一个异步锁,用于控制并发刷新:
// 异步锁,确保同一时间只有一个刷新Token操作执行 private readonly SemaphoreSlim _refreshTokenLock = new SemaphoreSlim(1, 1);
第二步:修改SendAsyncRequest方法
public async Task<HttpResponseMessage> SendAsyncRequest(string uri, string content, HttpMethod method, bool tryReauthorizeOn401 = true) { try { HttpRequestMessage rm = new HttpRequestMessage(method, uri); if (!string.IsNullOrWhiteSpace(content)) rm.Content = new StringContent(content, Encoding.UTF8, ContentType); HttpResponseMessage response = await httpClient.SendAsync(rm); if (response.StatusCode == HttpStatusCode.Unauthorized && tryReauthorizeOn401) { // 等待刷新锁,避免并发刷新 await _refreshTokenLock.WaitAsync(); try { // 先检查Token是否已被其他请求刷新(可选:比如对比当前Token是否还是过期的那个) bool refreshSuccess = await OnReauthorizeUser(); if (!refreshSuccess) return response; // 用新Token重发原请求,关闭重试避免循环 return await SendAsyncRequest(uri, content, method, false); } finally { // 释放锁,让其他等待的请求可以重试 _refreshTokenLock.Release(); } } return response; }catch(TaskCanceledException ex) { Debug.Assert(false, $"请求被取消: {ex.Message}"); // 可选:如果是刷新请求被意外取消,可以考虑重试一次 throw; }catch(Exception ex) { Debug.Assert(false, ex.Message); throw; } }
第三步:优化OnReauthorizeUser方法
确保刷新Token的请求不会触发重试逻辑,避免循环:
private async Task<bool> OnReauthorizeUser() { // 从本地存储获取RefreshToken var storedRefreshToken = GetStoredRefreshToken(); // 调用刷新接口,关闭重试开关 var refreshResponse = await SendAsyncRequest( "/tokens/refresh/", JsonConvert.SerializeObject(new { RefreshToken = storedRefreshToken }), HttpMethod.Post, tryReauthorizeOn401: false ); if (!refreshResponse.IsSuccessStatusCode) { // 刷新失败,比如RefreshToken过期,这里可以跳转到登录页 return false; } // 解析新的令牌 var newTokens = await refreshResponse.Content.ReadAsAsync<TokenResponse>(); // 更新本地存储的令牌 StoreNewTokens(newTokens.BearerToken, newTokens.RefreshToken); // 更新HttpClient的默认Authorization头,后续请求自动携带新Token httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", newTokens.BearerToken); return true; }
3. 流程说明
用户点击“查看我的文档”后,整个流程会变成:
- 发送
/documents/user/user-id请求,收到401响应 - 进入刷新逻辑,等待异步锁(如果有其他并发请求也会在这里排队)
- 调用
OnReauthorizeUser获取新的BearerToken,更新本地存储和HttpClient的请求头 - 释放异步锁,重发原请求
- 收到成功响应,展示文档给用户
额外优化建议
- 避免重复刷新:在进入刷新逻辑前,可以先检查当前的BearerToken是否已经被其他请求刷新过,若已更新则直接重试原请求
- 失败处理:如果RefreshToken也过期,应直接跳转到登录页面,避免用户看到无意义的错误
- 精准取消:如果需要取消请求,不要用全局的
CancelPendingRequests,而是为每个请求单独使用CancellationToken,精准控制取消范围
内容的提问来源于stack exchange,提问作者Adam Jachocki
相关产品推荐
相关产品推荐

