.NET 8 Blazor应用闲置后MSAL认证状态丢失问题排查
问题分析
你的问题核心是Blazor Server应用在长轮询模式下闲置30秒重连后出现403错误,手动导航却能恢复正常,原因可以归纳为以下几点:
- Blazor Server长轮询默认30秒超时,重连时会建立新的SignalR连接,服务器端会重新生成
AuthenticationState,但自定义声明未被正确注入新的ClaimsPrincipal。 - 自定义
InjectedClaimsAuthenticationStateProvider的判断逻辑仅在!authState.User.IsAuthorizedEntraTenant()时加载声明,重连时基础Entra认证状态仍存在,但自定义声明未被注入,触发授权策略的403规则。 - 缓存的自定义声明在重连时未被自动加载,且当前逻辑未在缓存缺失时主动调用下游API获取声明。
- 内存级的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
相关产品推荐
相关产品推荐

