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

Blazor Server对接外部API登录及令牌刷新的认证Cookie管理难题

Blazor Server 中基于外部API的Cookie认证与令牌刷新优化方案

背景

约半年前启动了一个Blazor Server项目,此前使用ASP.NET Framework 4.6.2,这是遗留系统迁移到现代框架的一部分,开发时边做边学Blazor。

当前认证实现概述

项目依赖独立的外部API提供登录和令牌刷新接口,当前实现流程:

  • 通过ASP.NET控制器处理Cookie写入:由于Blazor Server不总是走传统HTTP请求,必须通过控制器将认证Cookie附加到HTTP响应中,之后需要强制页面重载确保Cookie生效。
  • 自定义AuthenticationStateProvider:从Cookie中提取令牌管理认证状态,因为Blazor Server不能依赖JS的localStorage/sessionStorage,所以用HttpContextAccessor获取Cookie,但这也带来了后续问题。
  • HttpClientFactory + DelegatingHandler:自动给所有API请求附加令牌。

核心问题:令牌刷新后的Cookie同步问题

在DelegatingHandler中检测令牌过期,调用刷新接口获取新令牌并更新Cookie后,HttpContextAccessor仍然引用旧的HttpContext——因为没有新的HTTP请求触发Circuit刷新,只能强制页面重载,体验不佳。

优化方案建议

方案1:用Scoped服务存储认证状态(替代直接依赖HttpContextAccessor)

  • 初始加载同步Cookie到Scoped服务:在Circuit启动时(比如_Host.cshtml或自定义CircuitHandler),通过HttpContextAccessor读取初始Cookie,将令牌存入Scoped生命周期的IAuthStateService。该服务生命周期与Circuit绑定,跟随用户整个会话。
  • 更新令牌时同步服务与Cookie:
    1. 刷新令牌成功后,先更新IAuthStateService中的令牌值。
    2. 通过JS互操作更新浏览器端Cookie(Blazor Server注入IJSRuntime实现);如果是HttpOnly Cookie,需调用轻量后端接口更新。
    3. 调用AuthenticationStateProvider.NotifyAuthenticationStateChanged()通知组件状态变更,无需页面重载。
  • 后续请求直接从Scoped服务取令牌:DelegatingHandler和AuthenticationStateProvider都直接从IAuthStateService获取令牌,不再依赖HttpContextAccessor。

方案2:利用CircuitHandler监听Circuit刷新

创建自定义CircuitHandler,在OnCircuitOpenedAsync或OnConnectionUpAsync时,从HttpContext读取最新Cookie,同步到Scoped认证状态服务中。用户发起新HTTP请求(如刷新页面、打开新标签)时,服务会自动更新为最新Cookie值。


关键代码示例

1. Scoped认证状态服务

public interface IAuthStateService
{
    string AccessToken { get; }
    string RefreshToken { get; }
    Task UpdateTokens(string newAccessToken, string newRefreshToken);
}

public class AuthStateService : IAuthStateService
{
    private readonly IJSRuntime _jsRuntime;
    private readonly AuthenticationStateProvider _authStateProvider;

    public string AccessToken { get; private set; }
    public string RefreshToken { get; private set; }

    public AuthStateService(IJSRuntime jsRuntime, AuthenticationStateProvider authStateProvider)
    {
        _jsRuntime = jsRuntime;
        _authStateProvider = authStateProvider;
    }

    // 初始化时从HttpContext加载令牌(可在CircuitHandler中调用)
    public void InitializeTokens(string accessToken, string refreshToken)
    {
        AccessToken = accessToken;
        RefreshToken = refreshToken;
    }

    public async Task UpdateTokens(string newAccessToken, string newRefreshToken)
    {
        AccessToken = newAccessToken;
        RefreshToken = newRefreshToken;
        
        // 通过JS更新非HttpOnly Cookie
        await _jsRuntime.InvokeVoidAsync("updateAuthCookie", newAccessToken, newRefreshToken);
        
        // 通知认证状态变更
        await _authStateProvider.NotifyAuthenticationStateChanged(
            _authStateProvider.GetAuthenticationStateAsync());
    }
}

2. JS互操作更新Cookie(wwwroot/js/auth.js)

window.updateAuthCookie = function(accessToken, refreshToken) {
    const isSecure = window.location.protocol === 'https:';
    document.cookie = `AccessToken=${accessToken}; path=/; SameSite=Lax; secure=${isSecure}`;
    document.cookie = `RefreshToken=${refreshToken}; path=/; SameSite=Lax; secure=${isSecure}`;
    
    // 若为HttpOnly Cookie,替换为调用后端接口:
    // await fetch('/api/auth/update-cookie', {
    //     method: 'POST',
    //     headers: { 'Content-Type': 'application/json' },
    //     body: JSON.stringify({ accessToken, refreshToken })
    // });
};

3. 自定义DelegatingHandler

public class AuthDelegatingHandler : DelegatingHandler
{
    private readonly IAuthStateService _authStateService;
    private readonly IHttpClientFactory _httpClientFactory;

    public AuthDelegatingHandler(IAuthStateService authStateService, IHttpClientFactory httpClientFactory)
    {
        _authStateService = authStateService;
        _httpClientFactory = httpClientFactory;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // 附加当前令牌
        if (!string.IsNullOrEmpty(_authStateService.AccessToken))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _authStateService.AccessToken);
        }

        var response = await base.SendAsync(request, cancellationToken);

        // 检测令牌过期
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            var refreshClient = _httpClientFactory.CreateClient("RefreshApi");
            var refreshResponse = await refreshClient.PostAsJsonAsync("/refresh", 
                new { RefreshToken = _authStateService.RefreshToken });
            
            if (refreshResponse.IsSuccessStatusCode)
            {
                var newTokens = await refreshResponse.Content.ReadFromJsonAsync<TokenResponse>();
                await _authStateService.UpdateTokens(newTokens.AccessToken, newTokens.RefreshToken);
                
                // 重新发起原请求
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newTokens.AccessToken);
                response = await base.SendAsync(request, cancellationToken);
            }
        }

        return response;
    }
}

4. 服务注册(Program.cs)

builder.Services.AddScoped<IAuthStateService, AuthStateService>();
builder.Services.AddHttpClient("ExternalApi").AddHttpMessageHandler<AuthDelegatingHandler>();
builder.Services.AddHttpClient("RefreshApi").ConfigureHttpClient(c => 
    c.BaseAddress = new Uri(builder.Configuration["ApiUrls:Refresh"]));
builder.Services.AddTransient<AuthDelegatingHandler>();
builder.Services.AddHttpContextAccessor();

额外注意事项

  • HttpOnly Cookie处理:推荐将认证Cookie设为HttpOnly以防范XSS,此时JS无法直接修改,需通过后端接口更新Cookie,后续Circuit刷新时会自动同步。
  • Circuit重建同步:Blazor Server的Circuit可能因超时、重连重建,需在Circuit重建时从HttpContext重新读取Cookie到Scoped服务,可通过自定义CircuitHandler实现。
  • 状态一致性:确保AuthenticationStateProvider始终从Scoped服务获取令牌,避免与HttpContext状态不一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 00:17:14