Files
Atomx/Atomx.Admin/Atomx.Admin.Client/Utils/AuthHeaderHandler.cs
2025-12-04 04:20:59 +08:00

187 lines
7.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// WASM 模式下的请求拦截器DelegatingHandler
/// - 在每次请求时将 access token 附带 Authorization header
/// - 在 401 或响应头包含 "Token-Expired" 时尝试刷新(调用 /api/sign/refresh
/// - 防止并发刷新SemaphoreSlim
/// </summary>
public class AuthHeaderHandler : DelegatingHandler
{
private readonly ITokenProvider _tokenProvider;
private readonly NavigationManager _navigationManager;
private readonly ILogger<AuthHeaderHandler> _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<AuthHeaderHandler> logger,
ILocalStorageService localStorage,
IHttpClientFactory httpClientFactory)
{
_tokenProvider = tokenProvider;
_navigationManager = navigationManager;
_logger = logger;
_localStorage = localStorage;
_httpClientFactory = httpClientFactory;
}
protected override async Task<HttpResponseMessage> 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<string>(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<bool> TryRefreshTokenAsync(CancellationToken cancellationToken)
{
await _refreshLock.WaitAsync(cancellationToken);
try
{
var currentAccess = await _localStorage.GetItemAsync<string>(AccessTokenKey);
var currentRefresh = await _localStorage.GetItemAsync<string>(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<AuthResponse>(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<HttpRequestMessage> 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;
}
}
}