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

.NET 8 Blazor应用闲置后MSAL认证状态丢失问题排查

问题分析

你的问题核心是Blazor Server应用在长轮询模式下闲置30秒重连后出现403错误,手动导航却能恢复正常,原因可以归纳为以下几点:

  1. Blazor Server长轮询默认30秒超时,重连时会建立新的SignalR连接,服务器端会重新生成AuthenticationState,但自定义声明未被正确注入新的ClaimsPrincipal。
  2. 自定义InjectedClaimsAuthenticationStateProvider的判断逻辑仅在!authState.User.IsAuthorizedEntraTenant()时加载声明,重连时基础Entra认证状态仍存在,但自定义声明未被注入,触发授权策略的403规则。
  3. 缓存的自定义声明在重连时未被自动加载,且当前逻辑未在缓存缺失时主动调用下游API获取声明。
  4. 内存级的MSAL token缓存和自定义声明缓存,在连接重建时可能无法正确关联到用户会话。
解决方案

1. 修正AuthenticationStateProvider的声明注入逻辑

ClaimsPrincipal是不可变对象,直接修改原对象可能导致状态不一致;同时调整判断逻辑,确保重连时无论当前授权状态如何,都检查并注入自定义声明:

public class InjectedClaimsAuthenticationStateProvider(ILoggerFactory loggerFactory,
    IEntraUserContextFactory contextFactory,
    ICacheManager cache,
    ICacheKeyFactory keyFactory,
    IDownstreamApiClient downstreamApiClient) // 注入下游API客户端
    : ServerAuthenticationStateProvider, IAuthenticationStateProvider
{
    private readonly IDownstreamApiClient _downstreamApiClient = downstreamApiClient;

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var authState = await base.GetAuthenticationStateAsync();

        // 检查是否已注入自定义声明标识
        var hasInjectedIdentity = authState.User.Identities.Any(i => i.AuthenticationType == "claims-provider-injected");
        if (!hasInjectedIdentity)
        {
            var claims = await GetClaimsAsync(authState);
            if (claims.Any())
            {
                // 创建新的ClaimsPrincipal,添加自定义声明身份
                var newPrincipal = new ClaimsPrincipal(authState.User.Identity);
                newPrincipal.AddIdentity(new ClaimsIdentity(claims, "claims-provider-injected"));
                authState = new AuthenticationState(newPrincipal);
                // 更新全局认证状态
                base.SetAuthenticationState(Task.FromResult(authState));
            }
        }

        return authState;
    }

    // ... 其他方法保持不变,修改GetClaimsAsync如下 ...

    public async Task<Claim[]> GetClaimsAsync(AuthenticationState authenticationState)
    {
        var logger = loggerFactory.CreateLogger<InjectedClaimsAuthenticationStateProvider>();

        try
        {
            var entraClaims = authenticationState.User.Claims.ToArray();
            var context = contextFactory.GetUserContext(entraClaims);
            var key = GetCacheKey(context);

            if (cache.KeyExists(key))
            {
                return await cache.GetAsync<Claim[]>(key);
            }

            // 缓存缺失时,主动调用下游API获取声明
            var claimsResponse = await _downstreamApiClient.GetClaimsAsync(context);
            if (claimsResponse?.Claims.Any() == true)
            {
                var claims = claimsResponse.Claims.SelectMany(x =>
                {
                    var values = $"{x.Value}".Split(',', StringSplitOptions.TrimEntries);
                    return values.Select(v => new Claim(x.Key, v));
                }).ToArray();
                await cache.AddAsync(key, claims, TimeSpan.FromHours(1));
                return claims;
            }

            return Array.Empty<Claim>();
        }
        catch (MsalUiRequiredException ex)
        {
            logger.WriteException(ex, "获取声明需要用户交互");
            throw; // 抛出异常触发MSAL授权流程
        }
        catch (Exception e)
        {
            logger.WriteException(e, "获取声明时发生异常");
            return Array.Empty<Claim>();
        }
    }
}

2. 延长长轮询超时时间

减少重连频率,同时同步调整Front Door的超时设置:

// Program.cs中配置Blazor Hub
app.MapBlazorHub(options =>
{
    options.LongPolling.PollTimeout = TimeSpan.FromMinutes(5); // 延长至5分钟
});

3. 替换内存缓存为分布式缓存

避免应用实例回收或会话重建时丢失缓存数据,以Redis为例:

// Program.cs中注册MSAL分布式token缓存
services.AddMicrosoftIdentityWebAppAuthentication(configuration, "AzureAd", subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: detailedErrorsEnabled)
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddDownstreamEntraApi(ConfigurationConstants.ClaimsProviderName, configuration.GetDownstreamApiConfig(ConfigurationConstants.ClaimsProviderName))
    .WithHttpClient(ConfigurationConstants.ClaimsProviderName)
    .AddDistributedTokenCaches();

// 注册Redis分布式缓存
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = configuration.GetConnectionString("RedisConnection");
    options.InstanceName = "BlazorApp-Claims-";
});

4. 处理标签页休眠恢复场景

在客户端监听页面可见性变化,恢复时主动刷新认证状态:

<!-- App.razor -->
@inject IAuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavManager
@implements IDisposable

<Router AppAssembly="@typeof(App).Assembly">
    <!-- 现有路由内容 -->
</Router>

@code {
    private EventListener? _visibilityChangeListener;

    protected override void OnInitialized()
    {
        _visibilityChangeListener = _ => HandleVisibilityChange();
        document.addEventListener("visibilitychange", _visibilityChangeListener);
    }

    private async void HandleVisibilityChange()
    {
        if (document.visibilityState == "visible")
        {
            // 刷新认证状态
            await ((ServerAuthenticationStateProvider)AuthStateProvider).GetAuthenticationStateAsync();
            // 若当前在403页面,导航回首页
            if (NavManager.Uri.Contains("/403"))
            {
                NavManager.NavigateTo("/");
            }
        }
    }

    public void Dispose()
    {
        if (_visibilityChangeListener != null)
        {
            document.removeEventListener("visibilitychange", _visibilityChangeListener);
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 20:34:58