using Atomx.Admin.Client.Utils; using Atomx.Common.Models; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components; using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace Atomx.Admin.Client.Utils { /// /// 请求拦截器(WASM 模式主要运行在浏览器端) /// 功能: /// - 在每次请求时将 access token 附带到 Authorization header /// - 在收到 401 或响应头包含 "Token-Expired" 时,尝试使用本地保存的 refresh token 调用 /api/sign/refresh /// - 刷新成功:更新本地存储中的 accessToken/refreshToken,然后重试原请求一次 /// - 刷新失败:跳转到登录页 /// 说明: /// - 该实现依赖于 Blazored.LocalStorage(key 名称为 "accessToken" 和 "refreshToken"), /// 若你在项目中使用不同的键名,请统一替换。 /// - 为避免并发刷新,使用一个静态 SemaphoreSlim 进行序列化刷新请求。 /// public class AuthHeaderHandler : DelegatingHandler { private readonly ITokenProvider _tokenProvider; private readonly NavigationManager _navigationManager; private readonly ILogger _logger; private readonly ILocalStorageService _localStorage; private readonly IHttpClientFactory _httpClientFactory; private static readonly SemaphoreSlim _refreshLock = new(1, 1); // 本地存储键名(可按需修改) private const string AccessTokenKey = "accessToken"; private const string RefreshTokenKey = "refreshToken"; public AuthHeaderHandler( ITokenProvider tokenProvider, NavigationManager navigationManager, ILogger logger, ILocalStorageService localStorage, IHttpClientFactory httpClientFactory) { _tokenProvider = tokenProvider; _navigationManager = navigationManager; _logger = logger; _localStorage = localStorage; _httpClientFactory = httpClientFactory; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { try { // 1) 尝试从 token provider 获取并添加 Authorization header var token = await _tokenProvider.GetTokenAsync(); if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } else { _logger.LogDebug("No token from ITokenProvider for request {Url}", request.RequestUri); } var response = await base.SendAsync(request, cancellationToken); // 2) 检查 401 或 Token-Expired header if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.Headers.Contains("Token-Expired")) { _logger.LogInformation("Unauthorized or Token-Expired detected for {Url}", request.RequestUri); // 仅在浏览器(WASM)模式下自动刷新;在 Server 模式交由服务器端处理 if (OperatingSystem.IsBrowser()) { var refreshed = await TryRefreshTokenAsync(cancellationToken); if (refreshed) { // 获取新的 token 并重试请求(一次) var newToken = await _localStorage.GetItemAsync(AccessTokenKey); if (!string.IsNullOrEmpty(newToken)) { // 克隆原始请求(HttpRequestMessage 只能发送一次) var clonedRequest = await CloneHttpRequestMessageAsync(request, newToken); return await base.SendAsync(clonedRequest, cancellationToken); } } // 刷新失败,重定向登录 _navigationManager.NavigateTo("/account/login", true); } else { // Server 模式:记录日志,允许上层中间件决定下一步(不进行自动跳转) _logger.LogWarning("Unauthorized in server mode for {Url}", request.RequestUri); } } return response; } catch (Exception ex) { _logger.LogError(ex, "Error sending HTTP request to {Url}", request.RequestUri); throw; } } /// /// 尝试使用本地保存的 refresh token 调用刷新接口 /// API 约定: /// POST /api/sign/refresh /// Body: { token: "...", refreshToken: "..." } /// 返回: AuthResponse { Token, RefreshToken, TokenExpiry } /// private async Task TryRefreshTokenAsync(CancellationToken cancellationToken) { // 串行化刷新,防止多请求同时触发重复刷新 await _refreshLock.WaitAsync(cancellationToken); try { var currentAccess = await _localStorage.GetItemAsync(AccessTokenKey); var currentRefresh = await _localStorage.GetItemAsync(RefreshTokenKey); if (string.IsNullOrEmpty(currentAccess) || string.IsNullOrEmpty(currentRefresh)) { _logger.LogInformation("No local tokens to refresh"); return false; } // 调用刷新接口(使用 IHttpClientFactory 创建短期 HttpClient) var client = _httpClientFactory.CreateClient(); // 默认 client,建议在 Program.cs 中配置 BaseAddress var reqModel = new { token = currentAccess, refreshToken = currentRefresh }; var reqJson = JsonSerializer.Serialize(reqModel); using var req = new HttpRequestMessage(HttpMethod.Post, "api/sign/refresh") { Content = new StringContent(reqJson, Encoding.UTF8, "application/json") }; try { var resp = await client.SendAsync(req, cancellationToken); if (!resp.IsSuccessStatusCode) { _logger.LogWarning("Refresh request failed with status {Status}", resp.StatusCode); return false; } var json = await resp.Content.ReadAsStringAsync(cancellationToken); var authResp = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (authResp == null || string.IsNullOrEmpty(authResp.Token) || string.IsNullOrEmpty(authResp.RefreshToken)) { _logger.LogWarning("Invalid response from refresh endpoint"); return false; } // 保存新的 token(本地存储) await _localStorage.SetItemAsync(AccessTokenKey, authResp.Token, cancellationToken); await _localStorage.SetItemAsync(RefreshTokenKey, authResp.RefreshToken, cancellationToken); _logger.LogInformation("Token refreshed successfully"); return true; } catch (Exception ex) { _logger.LogError(ex, "Exception while requesting token refresh"); return false; } } finally { _refreshLock.Release(); } } /// /// 复制 HttpRequestMessage 并替换 Authorization header 为新的 token /// private static async Task CloneHttpRequestMessageAsync(HttpRequestMessage original, string newToken) { var clone = new HttpRequestMessage(original.Method, original.RequestUri); // Copy request content (if any) if (original.Content != null) { var ms = new MemoryStream(); await original.Content.CopyToAsync(ms).ConfigureAwait(false); ms.Position = 0; clone.Content = new StreamContent(ms); // copy content headers if (original.Content.Headers != null) { foreach (var h in original.Content.Headers) clone.Content.Headers.Add(h.Key, h.Value); } } // copy headers foreach (var header in original.Headers) clone.Headers.TryAddWithoutValidation(header.Key, header.Value); // set new auth header clone.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken); // copy properties foreach (var prop in original.Options) { // HttpRequestOptions 不直接序列化拷贝,这里通常无需处理 } return clone; } } }