You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor WASM如何在用户操作或收到401时校验JWT过期状态

问题根因

原有JWT校验逻辑仅绑定在MainLayout.razor的OnAfterRenderAsync生命周期,仅在页面首次加载、路由切换触发布局重渲染时才会执行。组件内的局部交互(如工单页切换标签加载数据)不会触发布局组件重渲染,自然不会运行过期校验,最终API请求携带过期token被服务端拒绝后,页面仅显示空状态,不会触发登录跳转。
另外两个原有设计的缺陷:

  • 纯客户端本地校验exp字段依赖客户端本地时间,若用户本地时间不准会出现校验偏差
  • 在GetAuthenticationStateAsync方法内直接调用LogoutAsync容易触发循环调用:登出逻辑会触发认证状态变更事件,框架会再次调用GetAuthenticationStateAsync获取最新状态,形成死循环风险
最优实现:全局拦截401响应

该方案覆盖所有场景的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 15:39:14