This commit is contained in:
2025-12-04 05:12:45 +08:00
parent 85f0cb613a
commit 5bdb04da15
8 changed files with 330 additions and 206 deletions

View File

@@ -13,6 +13,7 @@ namespace Atomx.Admin.Client.Utils
/// - 在每次请求时将 access token 附带 Authorization header
/// - 在 401 或响应头包含 "Token-Expired" 时尝试刷新(调用 /api/sign/refresh
/// - 防止并发刷新SemaphoreSlim
/// - 注意:刷新请求必须使用一个不包含本 handler 的 HttpClient避免循环
/// </summary>
public class AuthHeaderHandler : DelegatingHandler
{
@@ -45,6 +46,7 @@ namespace Atomx.Admin.Client.Utils
{
try
{
// 从 ITokenProvider 获取当前 access tokenWASM: ClientTokenProvider 从 localStorage 读取)
var token = await _tokenProvider.GetTokenAsync();
if (!string.IsNullOrEmpty(token))
{
@@ -57,6 +59,7 @@ namespace Atomx.Admin.Client.Utils
var response = await base.SendAsync(request, cancellationToken);
// 当发现未授权或后端标记 token 过期时,尝试刷新
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
response.Headers.Contains("Token-Expired"))
{
@@ -76,6 +79,7 @@ namespace Atomx.Admin.Client.Utils
}
}
// 刷新失败或无 token跳转到登录页强制刷新页面清除 SPA 状态)
_navigationManager.NavigateTo("/account/login", true);
}
else
@@ -93,6 +97,13 @@ namespace Atomx.Admin.Client.Utils
}
}
/// <summary>
/// 尝试刷新 token
/// 关键点:
/// - 使用命名 HttpClient "RefreshClient"(在 WASM Program 中注册,不包含本 handler避免递归
/// - 并发刷新时只有一个请求会实际触发刷新,其余等待结果
/// - 刷新成功后写入 localStorageaccessToken + refreshToken
/// </summary>
private async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken)
{
await _refreshLock.WaitAsync(cancellationToken);
@@ -107,11 +118,13 @@ namespace Atomx.Admin.Client.Utils
return false;
}
var client = _httpClientFactory.CreateClient();
var reqModel = new
// 使用不带本 handler 的 HttpClient 发送刷新请求,避免循环
var client = _httpClientFactory.CreateClient("RefreshClient");
var reqModel = new RefreshRequest
{
token = currentAccess,
refreshToken = currentRefresh
Token = currentAccess,
RefreshToken = currentRefresh
};
var reqJson = JsonSerializer.Serialize(reqModel);
using var req = new HttpRequestMessage(HttpMethod.Post, "api/sign/refresh")
@@ -125,6 +138,9 @@ namespace Atomx.Admin.Client.Utils
if (!resp.IsSuccessStatusCode)
{
_logger.LogWarning("Refresh request failed with status {Status}", resp.StatusCode);
// 如果刷新失败,移除本地 token防止无限重试
await _localStorage.RemoveItemAsync(AccessTokenKey);
await _localStorage.RemoveItemAsync(RefreshTokenKey);
return false;
}
@@ -137,9 +153,12 @@ namespace Atomx.Admin.Client.Utils
if (authResp == null || string.IsNullOrEmpty(authResp.Token) || string.IsNullOrEmpty(authResp.RefreshToken))
{
_logger.LogWarning("Invalid response from refresh endpoint");
await _localStorage.RemoveItemAsync(AccessTokenKey);
await _localStorage.RemoveItemAsync(RefreshTokenKey);
return false;
}
// 保存新的 tokens 到 localStorage
await _localStorage.SetItemAsync(AccessTokenKey, authResp.Token, cancellationToken);
await _localStorage.SetItemAsync(RefreshTokenKey, authResp.RefreshToken, cancellationToken);
@@ -158,10 +177,14 @@ namespace Atomx.Admin.Client.Utils
}
}
/// <summary>
/// 复制原始请求并用新的 token 替换 Authorization header
/// </summary>
private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage original, string newToken)
{
var clone = new HttpRequestMessage(original.Method, original.RequestUri);
// 复制内容(如果存在)
if (original.Content != null)
{
var ms = new MemoryStream();
@@ -176,9 +199,11 @@ namespace Atomx.Admin.Client.Utils
}
}
// 复制 headers
foreach (var header in original.Headers)
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
// 覆盖 Authorization
clone.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
return clone;