Blazor WASM如何在用户操作或收到401时校验JWT过期状态
原有JWT校验逻辑仅绑定在MainLayout.razor的OnAfterRenderAsync生命周期,仅在页面首次加载、路由切换触发布局重渲染时才会执行。组件内的局部交互(如工单页切换标签加载数据)不会触发布局组件重渲染,自然不会运行过期校验,最终API请求携带过期token被服务端拒绝后,页面仅显示空状态,不会触发登录跳转。
另外两个原有设计的缺陷:
- 纯客户端本地校验
exp字段依赖客户端本地时间,若用户本地时间不准会出现校验偏差 - 在
GetAuthenticationStateAsync方法内直接调用LogoutAsync容易触发循环调用:登出逻辑会触发认证状态变更事件,框架会再次调用GetAuthenticationStateAsync获取最新状态,形成死循环风险
该方案覆盖所有场景的API请求,以服务端的token校验结果为唯一判定标准,完全不受客户端时间、交互场景限制。
步骤1:实现统一未授权响应处理器
自定义DelegatingHandler拦截所有HTTP响应,检测到401状态码时统一执行清缓存、跳转登录逻辑,加入并发锁避免多个请求同时触发重复操作:
public class UnauthorizedResponseHandler : DelegatingHandler { private readonly ILocalStorageService _localStorage; private readonly NavigationManager _navigation; private static bool _isProcessingUnauthorized = false; public UnauthorizedResponseHandler(ILocalStorageService localStorage, NavigationManager navigation) { _localStorage = localStorage; _navigation = navigation; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !_isProcessingUnauthorized) { _isProcessingUnauthorized = true; try { // 清除本地存储的身份凭证 await _localStorage.RemoveItemAsync("token"); // 强制整页跳转登录页,避免SPA软跳转残留组件状态 _navigation.NavigateTo("/login?returnUrl=" + Uri.EscapeDataString(_navigation.Uri), forceLoad: true); } finally { _isProcessingUnauthorized = false; } } return response; } }
步骤2:注册处理器到HTTP请求管道
在Program.cs中替换原有HttpClient注册逻辑,将自定义处理器加入请求管道:
builder.Services.AddScoped<UnauthorizedResponseHandler>(); builder.Services.AddHttpClient("AppServerApi", client => { client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); }) .AddHttpMessageHandler<UnauthorizedResponseHandler>(); // 注册全局使用的HttpClient实例 builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("AppServerApi"));
移除MainLayout.razor中手动调用GetAuthenticationStateAsync的代码——Blazor框架会在需要获取认证状态时自动调用该方法,手动重复调用会产生无意义的性能消耗。
重构GetAuthenticationStateAsync方法,移除内部直接调用LogoutAsync的逻辑,仅做状态解析和校验,通过NotifyAuthenticationStateChanged方法通知框架认证状态变更:
public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var anonymousState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); var token = await _localStorageService.GetItemAsync<string>("token"); if (string.IsNullOrEmpty(token)) { return anonymousState; } IEnumerable<Claim> claims; try { claims = ParseClaimsFromJwt(token); } catch { // token格式非法直接清除 await _localStorageService.RemoveItemAsync("token"); return anonymousState; } var expClaimValue = claims.FirstOrDefault(c => c.Type == "exp")?.Value; if (!long.TryParse(expClaimValue, out var expUnixSeconds)) { await _localStorageService.RemoveItemAsync("token"); return anonymousState; } var expireUtcTime = DateTimeOffset.FromUnixTimeSeconds(expUnixSeconds).UtcDateTime; if (expireUtcTime <= DateTime.UtcNow) { await _localStorageService.RemoveItemAsync("token"); NotifyAuthenticationStateChanged(Task.FromResult(anonymousState)); return anonymousState; } var validIdentity = new ClaimsIdentity(claims, "jwt"); return new AuthenticationState(new ClaimsPrincipal(validIdentity)); }
在App.razor中配置AuthorizeRouteView,结合重定向组件实现认证状态变更时的自动跳转,不需要在各个页面、组件单独写跳转逻辑:
<Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> <RedirectToLogin /> </NotAuthorized> </AuthorizeRouteView> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> <NotFound> <PageTitle>Not Found</PageTitle> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">请求的页面不存在</p> </LayoutView> </NotFound> </Router>
其中RedirectToLogin组件实现如下:
@inject NavigationManager Navigation @code { protected override void OnInitialized() { var returnUrl = Uri.EscapeDataString(Navigation.Uri); Navigation.NavigateTo($"/login?returnUrl={returnUrl}", forceLoad: true); } }
内容的提问来源于stack exchange,提问作者CavalornGothic

