LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

Blazor 里 JWT 过期,用户正下单就被踢

freeflydom
2026年10月10日 11:59 本文热度 210

大家好,我是码农刚子。

用户在页面里填了一整页表单,点了提交——啪,跳回登录页。JWT 过期了,刷新令牌没接上,所有数据白填。这篇文章讲 Blazor 里怎么正确实现 JWT 刷新令牌,让用户无感续期。

先说为什么 Blazor 里这个问题特别恶心。

在普通 SPA(React/Vue)里,JWT 存在 localStorage,请求拦截器统一加 header,401 时自动刷新——一套 Axios 拦截器搞定。但 Blazor 不一样:Blazor Server 跑在服务端,Blazor WebAssembly 跑在浏览器里,两种模式下 token 的存储位置、刷新时机、信号传递方式完全不同。

而且 Blazor 的组件状态是活的——用户在页面上操作时,组件不会重新渲染,你没法靠"页面刷新"来触发重新认证。必须在 HTTP 层静默处理。

01 为什么 JWT 过期会"踢人"

JWT 有过期时间(exp claim)。过期后,API 服务器验证失败返回 401。如果你的应用不做刷新,401 的处理就是跳登录页。

问题是——JWT 的过期时间通常很短(15-30 分钟),这是安全设计。长过期时间 = token 被盗后攻击窗口大。所以业界做法是短 access token + 长 refresh token:

Access Token  — 15-30 分钟过期,放 Authorization header
Refresh Token — 7-30 天过期,存安全位置(httpOnly cookie / protected storage)
                    ↓ access token 过期时,用 refresh token 换新的

用户无感续期的流程是:

① 请求 API → 带 access token ② API 返回 401 → access token 过期了 ③ 拦截 401 → 用 refresh token 调 /refresh 端点换新 access token ④ 拿到新 token → 重放原始请求 ⑤ 用户完全无感,表单数据不丢

坑在哪?Blazor 的 HttpClient 不是浏览器 fetch,不能简单套 Axios 那套。Blazor Server 的 HttpClient 是 HttpClientFactory 创建的,Blazor WebAssembly 的 HttpClient 是 WASM runtime 里的——两者拦截 401 的方式完全不同。

02 Blazor Server:DelegatingHandler 拦截

Blazor Server 跑在服务端,HttpClient 可以套 DelegatingHandler——这是 .NET 原生的 HTTP 管道机制,所有请求经过 handler 链。

核心思路:自定义一个 AuthTokenHandler,在 SendAsync 里自动加 token,401 时自动刷新。

public class AuthTokenHandler : DelegatingHandler
{
    private readonly TokenService _tokenService;
    public AuthTokenHandler(TokenService tokenService)
    {
        _tokenService = tokenService;
    }
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        // 1. 加 access token
        var token = await _tokenService.GetAccessTokenAsync();
        if (!string.IsNullOrEmpty(token))
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token);
        // 2. 发请求
        var response = await base.SendAsync(request, ct);
        // 3. 401 → 尝试刷新
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            var refreshed = await _tokenService
                .TryRefreshTokenAsync();
            if (refreshed)
            {
                // 4. 重放原始请求
                var newToken = await _tokenService
                    .GetAccessTokenAsync();
                request.Headers.Authorization =
                    new AuthenticationHeaderValue(
                        "Bearer", newToken);
                // 注意:HttpClient 会缓存请求,
                // 需要克隆一份再发
                response.Dispose();
                var retry = await CloneAndSendAsync(
                    request, ct);
                return retry;
            }
        }
        return response;
    }
    private async Task<HttpResponseMessage> CloneAndSendAsync(
        HttpRequestMessage req, CancellationToken ct)
    {
        var clone = new HttpRequestMessage(req.Method, req.RequestUri);
        // 克隆 content(如果有的话)
        if (req.Content != null)
        {
            var bytes = await req.Content.ReadAsByteArrayAsync(ct);
            clone.Content = new ByteArrayContent(bytes);
        }
        foreach (var h in req.Headers)
            clone.Headers.TryAddWithoutValidation(
                h.Key, h.Value);
        return await base.SendAsync(clone, ct);
    }
}

关键细节:

请求克隆——HttpRequestMessage 发送后不能复用,必须克隆一份再重发。这是最容易漏的一步,漏了直接报"请求已发送"异常。

并发刷新锁——多个请求同时 401 会触发多次刷新,refresh token 可能被消耗多次(如果是一次性 token)。加一个 SemaphoreSlim 保证只刷新一次,其他请求等结果。

刷新失败处理——refresh token 也过期了,跳登录页。但 Blazor Server 里不能直接 NavigationManager.NavigateTo("/login")(那是客户端导航),需要通过 JS Interop 调 location.href 做整页跳转。

// TokenService 里的并发刷新
private static readonly SemaphoreSlim _refreshLock = new(1, 1);
public async Task<bool> TryRefreshTokenAsync()
{
    await _refreshLock.WaitAsync();
    try
    {
        // 双重检查:也许别的线程刚刷过
        if (IsAccessTokenValid())
            return true;
        var refreshReq = new { RefreshToken = _refreshToken };
        var resp = await _httpClient.PostAsJsonAsync(
            "/api/auth/refresh", refreshReq);
        if (!resp.IsSuccessStatusCode)
        {
            // refresh token 也过期 → 强制登出
            await _jsRuntime.InvokeVoidAsync(
                "location.replace", "/login?expired=1");
            return false;
        }
        var tokens = await resp.Content
            .ReadFromJsonAsync<TokenResponse>();
        _accessToken = tokens.AccessToken;
        _refreshToken = tokens.RefreshToken;
        return true;
    }
    finally
    {
        _refreshLock.Release();
    }
}

03 Blazor WebAssembly:自定义 HttpMessageHandler

WASM 模式下思路一样,但实现有区别——token 存在浏览器里,而且没有 SemaphoreSlim 的跨组件同步问题(因为 WASM 是单线程的)。

存储选择:

// Program.cs — 注册带 token 拦截的 HttpClient
builder.Services.AddScoped<AuthTokenHandler>();
builder.Services.AddScoped(sp =>
{
    var handler = sp.GetRequiredService<AuthTokenHandler>();
    handler.InnerHandler = new HttpClientHandler();
    return new HttpClient(handler)
    {
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    };
});
// Token 存 protected localStorage
// (Blazor WASM 的 ProtectedLocalStorage 在 Server 端,
//   WASM 用 IJSRuntime 直接调 localStorage API)
builder.Services.AddSingleton<ITokenStorage,
    LocalTokenStorage>();

WASM 里的 handler 和 Server 版几乎一样,区别在刷新失败时的跳转——WASM 可以直接用 NavigationManager 做客户端导航,不需要 JS Interop。

WASM 专属坑:localStorage 是异步的,但 DelegatingHandler.SendAsync 也是异步的,没问题。但如果你在 OnInitializedAsync 里调 API,刷新 + 重放的延迟会导致组件渲染两次——务必在组件里处理 Loading 状态,否则用户会看到一闪而过的错误。

04 两种模式都要注意的三件事

① refresh token 的存储位置

Blazor Server:存在服务端内存 / Session / Redis。别存 cookie——Blazor Server 用 SignalR 连接,cookie 跟着连接走,断线重连会丢。

Blazor WASM:存 localStorage。XSS 风险?WASM 默认不做 dangerouslySetInnerHTML,XSS 攻击面比传统 SPA 小。但如果你的 WASM 应用加载了第三方 JS 模块,仍有风险。折中方案:access token 存内存,refresh token 存 localStorage,JS 模块拿不到内存里的 access token。

② 刷新端点的设计

POST /api/auth/refresh
Body: { "refreshToken": "xxx" }
Response: {
    "accessToken": "新 access token",
    "refreshToken": "新 refresh token"  // 轮换!
}

refresh token 必须轮换——每次刷新后旧的失效,返回新的。不轮换 = refresh token 被盗后永久有效。进一步可以加 refresh token rotation + reuse detection:如果检测到旧 token 被使用,说明可能被窃取,立即吊销整个 token 家族。

③ 预刷新:别等 401 再刷

等 401 再刷的问题是——那个请求已经失败了。虽然重放能恢复,但用户可能看到一瞬间的错误状态。更好的做法是预判过期:在发请求前检查 access token 的 exp claim,如果快过期了(比如 5 分钟内),先刷新再发请求。

// TokenService 里加预判
public async Task<string> GetAccessTokenAsync()
{
    if (_accessToken != null)
    {
        var jwt = new JwtSecurityToken(_accessToken);
        // 过期前 5 分钟刷新
        if (jwt.ValidTo > DateTime.UtcNow.AddMinutes(5))
            return _accessToken;
    }
    // 快过期了 → 主动刷新
    await TryRefreshTokenAsync();
    return _accessToken;
}

这样 AuthTokenHandler 里 GetAccessTokenAsync 自带预判,401 分支只是兜底——正常情况下永远不会走到 401 分支。

Blazor JWT 刷新令牌,核心就四步: DelegatingHandler 拦截 → 401 自动刷新 → 请求重放 → 预判过期抢先刷 用户从头到尾无感,表单数据不丢。

阅读原文:点击这里


该文章在 2026/10/10 11:59:30 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-9  粤公网安备44030602007207号