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

Blazor Server集成OpenID Connect的访问令牌刷新问题咨询

Blazor Server认证令牌反复被过期值覆盖问题解决方案

核心根因

  • TokenProvider实例反复清空基本是预渲染(Prerendering)导致:预渲染阶段会生成独立的临时DI容器,等SignalR电路正式建立后会重新创建一套Scoped服务,预渲染阶段写入的令牌状态会直接丢弃,导致每次电路初始化都会触发shouldSetInitialValues判断为true。
  • Blazor Server Scoped服务生命周期与电路(Circuit)绑定,而非传统HTTP请求:_Host.cshtml中的令牌读取逻辑仅在电路初始化的首次HTTP请求时执行,后续组件导航、交互都不会触发该逻辑。且内存中刷新得到的新令牌不会自动同步回认证Cookie,后续硬刷新页面、新建电路时从HttpContext拿到的永远是登录时写入的旧过期令牌。
  • 现有初始化逻辑缺少过期判断:仅校验令牌是否为null,没有校验剩余有效期,过期令牌会直接覆盖内存中的有效值。

实现步骤

1. 修复预渲染状态丢失问题

如果不需要预渲染,直接在_Host.cshtml中将组件的render-mode设置为Server即可关闭预渲染:

<component type="typeof(App)" render-mode="Server" />

如果需要保留预渲染,调整App.razor的初始化逻辑,等电路正式建立、认证状态就绪后再执行令牌赋值,跳过预渲染阶段的无效初始化,同时增加令牌有效期判断:

[CascadingParameter]
private Task<AuthenticationState>? AuthenticationStateTask { get; set; }

protected override async Task OnInitializedAsync()
{
    if (AuthenticationStateTask != null)
    {
        await AuthenticationStateTask;
    }

    // 令牌为空、或剩余有效期不足5分钟时触发重新赋值
    var needRefresh = TokenProvider.AccessToken == null 
        || TokenProvider.RefreshToken == null 
        || TokenProvider.ExpiresAtUtc <= DateTimeOffset.UtcNow.AddMinutes(5);

    if (needRefresh)
    {
        // 初始令牌有效则直接赋值,否则直接走刷新逻辑
        if (InitialState != null && InitialState.ExpiresAtUtc > DateTimeOffset.UtcNow.AddMinutes(5))
        {
            TokenProvider.AccessToken = InitialState.AccessToken;
            TokenProvider.RefreshToken = InitialState.RefreshToken;
            TokenProvider.IdToken = InitialState.IdToken;
            TokenProvider.ExpiresAtUtc = InitialState.ExpiresAtUtc;
        }
        else
        {
            await TokenProvider.RefreshTokenAsync();
        }
    }

    await base.OnInitializedAsync();
}

2. 刷新令牌后同步更新认证Cookie

不要将新令牌仅存在内存中,刷新成功后必须同步更新认证Cookie存储的令牌,否则后续新建电路时永远会拿到旧的过期令牌。封装令牌持久化方法:

public async Task PersistTokensToCookieAsync(string accessToken, string refreshToken, string idToken, DateTimeOffset expiresAt)
{
    var httpContext = _httpContextAccessor.HttpContext;
    if (httpContext == null) return;

    var authResult = await httpContext.AuthenticateAsync();
    if (!authResult.Succeeded) return;

    var tokenList = new List<AuthenticationToken>
    {
        new() { Name = "access_token", Value = accessToken },
        new() { Name = "refresh_token", Value = refreshToken },
        new() { Name = "id_token", Value = idToken }
    };

    authResult.Properties.StoreTokens(tokenList);
    authResult.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o"));

    await httpContext.SignInAsync(authResult.Principal!, authResult.Properties);
}

3. 实现带并发锁的自动刷新逻辑

在TokenProvider中封装令牌获取方法,每次请求接口前检查有效期,过期自动刷新,加锁避免多个请求同时触发刷新:

public class TokenProvider
{
    private readonly IOpenIdConnectService _oidcService; // 自行封装的OIDC令牌交互服务
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);

    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? IdToken { get; set; }
    public DateTimeOffset ExpiresAtUtc { get; set; }

    public async Task<string> GetValidAccessTokenAsync()
    {
        // 剩余有效期超过5分钟直接返回
        if (ExpiresAtUtc > DateTimeOffset.UtcNow.AddMinutes(5))
        {
            return AccessToken!;
        }

        await _refreshLock.WaitAsync();
        try
        {
            // 锁等待后二次校验,避免重复刷新
            if (ExpiresAtUtc > DateTimeOffset.UtcNow.AddMinutes(5))
            {
                return AccessToken!;
            }

            // 调用身份提供者刷新端点获取新令牌
            var newTokens = await _oidcService.RefreshTokenAsync(RefreshToken!);
            
            // 更新内存状态
            AccessToken = newTokens.AccessToken;
            RefreshToken = newTokens.RefreshToken;
            IdToken = newTokens.IdToken;
            ExpiresAtUtc = newTokens.ExpiresAtUtc;

            // 同步更新Cookie
            await PersistTokensToCookieAsync(AccessToken, RefreshToken, IdToken, ExpiresAtUtc);

            return AccessToken!;
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    // 初始化时调用的无锁刷新逻辑
    public async Task RefreshTokenAsync()
    {
        var newTokens = await _oidcService.RefreshTokenAsync(RefreshToken!);
        AccessToken = newTokens.AccessToken;
        RefreshToken = newTokens.RefreshToken;
        IdToken = newTokens.IdToken;
        ExpiresAtUtc = newTokens.ExpiresAtUtc;
        await PersistTokensToCookieAsync(AccessToken, RefreshToken, IdToken, ExpiresAtUtc);
    }
}

4. 服务注册注意事项

  • TokenProvider必须注册为Scoped生命周期,禁止注册为Singleton,否则会出现多用户令牌串用的问题。
  • 提前在Program.cs中注册HttpContext访问服务:
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<TokenProvider>();

注意:除了_Host.cshtml页面和令牌持久化场景,不要在Blazor组件中随意使用IHttpContextAccessor获取HttpContext,组件运行阶段HttpContext默认是null,会触发空引用异常。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 15:30:05