chore
This commit is contained in:
@@ -20,6 +20,6 @@
|
||||
/// <summary>
|
||||
/// 是否记住我
|
||||
/// </summary>
|
||||
public bool SaveMe { get; set; }
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
@layout EmptyLayout
|
||||
@inject ILogger<Login> Logger
|
||||
|
||||
|
||||
<PageTitle>登录</PageTitle>
|
||||
|
||||
@if (!dataLoaded)
|
||||
@@ -67,7 +66,6 @@ else
|
||||
|
||||
private bool _isLoading = false;
|
||||
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (OperatingSystem.IsBrowser())
|
||||
@@ -85,12 +83,9 @@ else
|
||||
if (firstRender)
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (authState.User.Identity != null)
|
||||
if (authState.User.Identity != null && authState.User.Identity.IsAuthenticated)
|
||||
{
|
||||
if (authState.User.Identity.IsAuthenticated)
|
||||
{
|
||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||
}
|
||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||
}
|
||||
}
|
||||
if (!dataLoaded)
|
||||
@@ -102,32 +97,49 @@ else
|
||||
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
if (form.Validate())
|
||||
if (!form.Validate()) return;
|
||||
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// 请求后端登录接口,后端返回 ApiResult<AuthResponse>
|
||||
var api = "/api/sign/in";
|
||||
var result = await HttpService.Post<ApiResult<string>>(api, login);
|
||||
if (result.Success)
|
||||
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
|
||||
if (result.Success && result.Data != null)
|
||||
{
|
||||
Console.WriteLine("请求api成功");
|
||||
if (!string.IsNullOrEmpty(result.Data))
|
||||
var auth = result.Data;
|
||||
|
||||
// 保存 access + refresh 到 localStorage(WASM 场景)
|
||||
await localStorage.SetItemAsync("accessToken", auth.Token);
|
||||
await localStorage.SetItemAsync("refreshToken", auth.RefreshToken);
|
||||
|
||||
// 更新客户端 AuthenticationState(调用自定义 Provider 更新方法)
|
||||
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
|
||||
{
|
||||
await localStorage.SetItemAsStringAsync(StorageKeys.AccessToken, result.Data);
|
||||
await localStorage.SetItemAsStringAsync("refreshToken", result.Data);
|
||||
var authState = (AuthStateProvider as PersistentAuthenticationStateProvider);
|
||||
if (authState != null)
|
||||
{
|
||||
authState.UpdateAuthenticationState(result.Data);
|
||||
}
|
||||
Logger.LogInformation($"登录成功跳转目标,{ReturnUrl}");
|
||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||
// provider 仅需要 access token 更新来触发 UI 更新
|
||||
provider.UpdateAuthenticationState(auth.Token);
|
||||
}
|
||||
|
||||
Logger.LogInformation("登录成功,跳转: {ReturnUrl}", ReturnUrl);
|
||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message });
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "登录失败");
|
||||
ModalService.Error(new ConfirmOptions() { Title = "错误", Content = "登录异常,请稍后重试" });
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnPasswordKeyDown(KeyboardEventArgs value)
|
||||
|
||||
@@ -6,26 +6,26 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
// ע<>᱾<EFBFBD>ش洢<D8B4><E6B4A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// ע<>᱾<EFBFBD>ش洢<D8B4><E6B4A2>WASM ʹ<><CAB9> localStorage <20><><EFBFBD><EFBFBD> tokens<EFBFBD><EFBFBD>
|
||||
builder.Services.AddBlazoredLocalStorageAsSingleton();
|
||||
|
||||
// <20><>Ȩ/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <20><>Ȩ/<2F><><EFBFBD>ݣ<EFBFBD>WASM ʹ<><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthenticationStateProvider<EFBFBD><EFBFBD>
|
||||
builder.Services.AddAuthorizationCore();
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
|
||||
|
||||
// Ȩ<><C8A8> & <20><><EFBFBD>ػ<EFBFBD>
|
||||
// Ȩ<><C8A8> & <20><><EFBFBD>ػ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD>֣<EFBFBD>
|
||||
builder.Services.AddScoped<IPermissionService, ClientPermissionService>();
|
||||
builder.Services.AddScoped<IconsExtension>();
|
||||
builder.Services.AddScoped<ILocalizationService, LocalizationClientService>();
|
||||
|
||||
// Token provider<65><72>WASM<53><4D>
|
||||
// Token provider<65><72>WASM<53><4D>: <20><> localStorage <20><>ȡ access token
|
||||
builder.Services.AddScoped<ITokenProvider, ClientTokenProvider>();
|
||||
|
||||
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler
|
||||
builder.Services.AddScoped<AuthHeaderHandler>();
|
||||
|
||||
// ע<EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HttpClient<6E><74><EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> API <20><><EFBFBD><EFBFBD>ʹ<EFBFBD>ã<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <20><><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD> HttpClient<6E><74>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>ͳһʹ<EFBFBD><EFBFBD> ApiClient<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD>
|
||||
var apiBase = builder.Configuration["WebApi:ServerUrl"] ?? builder.HostEnvironment.BaseAddress;
|
||||
builder.Services.AddHttpClient("ApiClient", client =>
|
||||
{
|
||||
@@ -33,8 +33,14 @@ builder.Services.AddHttpClient("ApiClient", client =>
|
||||
})
|
||||
.AddHttpMessageHandler<AuthHeaderHandler>();
|
||||
|
||||
// Ϊ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD> HttpClient<EFBFBD><EFBFBD>AuthHeaderHandler <20>ڲ<EFBFBD> CreateClient() ʹ<><CAB9>Ĭ<EFBFBD>Ϲ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// Ҳע<EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD> HttpClient <20><> BaseAddress
|
||||
// ע<><D7A2>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <20><> HttpClient<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ˢ<EFBFBD><EFBFBD> token<65><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD>
|
||||
// <EFBFBD><EFBFBD> client <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <20><>ʹ<EFBFBD><CAB9> "RefreshClient"
|
||||
builder.Services.AddHttpClient("RefreshClient", client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBase);
|
||||
});
|
||||
|
||||
// Ĭ<><C4AC> HttpClient<6E><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2> HttpClient<6E><74>
|
||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"));
|
||||
|
||||
// <20><> WASM DI <20><>ע<EFBFBD><D7A2> HttpService<63><65>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD><EFBFBD> HttpClient ʵ<><CAB5>
|
||||
|
||||
@@ -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 token(WASM: 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),避免递归
|
||||
/// - 并发刷新时只有一个请求会实际触发刷新,其余等待结果
|
||||
/// - 刷新成功后写入 localStorage(accessToken + 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;
|
||||
|
||||
Reference in New Issue
Block a user