using Atomx.Admin.Client.Services; 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 模式下的请求拦截器(DelegatingHandler) /// - 在每次请求时将 access token 附带 Authorization header /// - 在 401 或响应头包含 "Token-Expired" 时尝试刷新(调用 /api/sign/refresh) /// - 防止并发刷新(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 { 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); if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.Headers.Contains("Token-Expired")) { _logger.LogInformation("Unauthorized or Token-Expired detected for {Url}", request.RequestUri); if (OperatingSystem.IsBrowser()) { var refreshed = await TryRefreshTokenAsync(cancellationToken); if (refreshed) { var newToken = await _localStorage.GetItemAsync(AccessTokenKey); if (!string.IsNullOrEmpty(newToken)) { var clonedRequest = await CloneHttpRequestMessageAsync(request, newToken); return await base.SendAsync(clonedRequest, cancellationToken); } } _navigationManager.NavigateTo("/account/login", true); } else { _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; } } 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; } var client = _httpClientFactory.CreateClient(); 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; } 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(); } } private static async Task CloneHttpRequestMessageAsync(HttpRequestMessage original, string newToken) { var clone = new HttpRequestMessage(original.Method, original.RequestUri); if (original.Content != null) { var ms = new MemoryStream(); await original.Content.CopyToAsync(ms).ConfigureAwait(false); ms.Position = 0; clone.Content = new StreamContent(ms); if (original.Content.Headers != null) { foreach (var h in original.Content.Headers) clone.Content.Headers.Add(h.Key, h.Value); } } foreach (var header in original.Headers) clone.Headers.TryAddWithoutValidation(header.Key, header.Value); clone.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken); return clone; } } }