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

.NET6下Blazor Server+Web API实现Refresh Token拦截请求防登出

Blazor Server 刷新登出与HTTP请求拦截实现方案

首先明确:用refresh token解决刷新页面自动登出的思路是完全正确的。你之前自定义中间件只执行一次,是Blazor Server的运行机制导致的:Blazor Server通过SignalR长连接维持电路(Circuit),ASP.NET Core管道中间件仅在电路初始化阶段执行一次,后续组件内发起的所有HTTP请求都不会重复经过该中间件,自然无法满足每次请求拦截的需求。
以下是分步骤的可落地方案:

一、解决页面刷新丢失登录态问题

Blazor Server的Scoped服务生命周期绑定到电路连接,内存中存储的令牌会在页面刷新、电路重建时被清空,必须将令牌持久化到浏览器加密存储中:

  • 引入Nuget包Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage,使用加密的本地存储避免令牌明文泄露
  • 新增内存令牌管理类,单电路实例内共享当前用户的令牌信息:
    public class TokenProvider
    {
        public string? AccessToken { get; set; }
        public string? RefreshToken { get; set; }
        public DateTimeOffset ExpireAt { get; set; }
    
        public void Clear()
        {
            AccessToken = null;
            RefreshToken = null;
            ExpireAt = DateTimeOffset.MinValue;
        }
    }
    
  • 自定义电路处理器,每次电路初始化(含页面刷新触发的新电路)时自动从本地存储加载令牌,恢复登录态:
    public class AuthCircuitHandler : CircuitHandler
    {
        private readonly ProtectedLocalStorage _localStorage;
        private readonly TokenProvider _tokenProvider;
        private const string StorageKey = "app_auth_tokens";
    
        public AuthCircuitHandler(ProtectedLocalStorage localStorage, TokenProvider tokenProvider)
        {
            _localStorage = localStorage;
            _tokenProvider = tokenProvider;
        }
    
        public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken ct)
        {
            var storageResult = await _localStorage.GetAsync<TokenStoreDto>(StorageKey);
            if (storageResult.Success)
            {
                _tokenProvider.AccessToken = storageResult.Value.AccessToken;
                _tokenProvider.RefreshToken = storageResult.Value.RefreshToken;
                _tokenProvider.ExpireAt = storageResult.Value.ExpireAt;
            }
            await base.OnCircuitOpenedAsync(circuit, ct);
        }
    }
    
  • 在Program.cs中注册相关服务:
    builder.Services.AddScoped<TokenProvider>();
    builder.Services.AddProtectedLocalStorage();
    builder.Services.AddScoped<CircuitHandler, AuthCircuitHandler>();
    
    登录、刷新令牌成功后,记得同时更新TokenProvider的内存值和本地加密存储中的令牌数据。

二、实现全局HTTP请求拦截,自动触发令牌刷新

不要使用管道中间件做请求拦截,正确的实现方式是自定义DelegatingHandler,绑定到你用于调用后端API的HttpClient上,该Handler会在每次HttpClient发起请求时自动执行:

  • 编写自定义令牌刷新拦截器:
    public class RefreshTokenDelegatingHandler : DelegatingHandler
    {
        private readonly IRefreshTokenService _refreshTokenService;
        private readonly TokenProvider _tokenProvider;
        private readonly ProtectedLocalStorage _localStorage;
        private const string StorageKey = "app_auth_tokens";
    
        public RefreshTokenDelegatingHandler(
            IRefreshTokenService refreshTokenService,
            TokenProvider tokenProvider,
            ProtectedLocalStorage localStorage)
        {
            _refreshTokenService = refreshTokenService;
            _tokenProvider = tokenProvider;
            _localStorage = localStorage;
        }
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
        {
            // 提前1分钟判断令牌是否过期,避免请求到后端才发现过期
            if (_tokenProvider.ExpireAt <= DateTimeOffset.UtcNow.AddMinutes(1) 
                && !string.IsNullOrEmpty(_tokenProvider.RefreshToken))
            {
                var refreshResult = await _refreshTokenService.RefreshTokenAsync(_tokenProvider.RefreshToken);
                if (refreshResult.IsSuccess)
                {
                    // 更新内存令牌
                    _tokenProvider.AccessToken = refreshResult.Data.AccessToken;
                    _tokenProvider.RefreshToken = refreshResult.Data.RefreshToken;
                    _tokenProvider.ExpireAt = refreshResult.Data.ExpireAt;
                    // 同步更新持久化存储
                    await _localStorage.SetAsync(StorageKey, refreshResult.Data);
                }
                else
                {
                    // 刷新失败清空令牌,后续可配合导航服务跳转到登录页
                    _tokenProvider.Clear();
                    await _localStorage.DeleteAsync(StorageKey);
                }
            }
    
            // 附加有效令牌到请求头
            if (!string.IsNullOrEmpty(_tokenProvider.AccessToken))
            {
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
                    "Bearer", _tokenProvider.AccessToken);
            }
    
            var response = await base.SendAsync(request, ct);
    
            // 兼容边界情况:令牌刚好过期触发401时,再尝试刷新一次重发请求
            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized 
                && !string.IsNullOrEmpty(_tokenProvider.RefreshToken))
            {
                var refreshResult = await _refreshTokenService.RefreshTokenAsync(_tokenProvider.RefreshToken);
                if (refreshResult.IsSuccess)
                {
                    _tokenProvider.AccessToken = refreshResult.Data.AccessToken;
                    _tokenProvider.RefreshToken = refreshResult.Data.RefreshToken;
                    _tokenProvider.ExpireAt = refreshResult.Data.ExpireAt;
                    await _localStorage.SetAsync(StorageKey, refreshResult.Data);
    
                    // 克隆请求用新令牌重发
                    request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
                        "Bearer", _tokenProvider.AccessToken);
                    response = await base.SendAsync(request, ct);
                }
                else
                {
                    _tokenProvider.Clear();
                    await _localStorage.DeleteAsync(StorageKey);
                }
            }
    
            return response;
        }
    }
    
  • 在Program.cs中注册HttpClient时绑定该拦截器:
    builder.Services.AddScoped<RefreshTokenDelegatingHandler>();
    builder.Services.AddHttpClient("BackendApi", client =>
    {
        client.BaseAddress = new Uri("你的后端Web API根地址");
    })
    .AddHttpMessageHandler<RefreshTokenDelegatingHandler>();
    
    后续所有通过该HttpClient发起的请求,都会自动走令牌检查、刷新、附加请求头的逻辑,不需要在每个组件里单独处理。

关键注意事项

  • 所有和令牌、请求拦截相关的服务(TokenProvider、RefreshTokenDelegatingHandler、RefreshTokenService)必须注册为Scoped生命周期,禁止使用Singleton,否则会出现多用户令牌串号的严重问题
  • 不要在Blazor Server中使用IHttpContextAccessor获取请求上下文做令牌处理,电路建立后HttpContext就会释放,后续调用会出现空引用或上下文错误
  • 如果使用Blazor内置的认证状态提供程序,记得在令牌更新时同步通知认证状态变化,避免UI层登录状态显示不同步

内容的提问来源于stack exchange,提问作者Ovis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 23:40:37