Blazor WASM服务端组件身份验证状态丢失问题排查
需要将基于Identity Server的Open Id认证宿主应用的Auth Token共享给页面内嵌的Blazor WASM客户端,已参考Visual Studio默认Blazor WASM项目配置PersistingServerAuthenticationStateProvider等组件,但调试发现:
PersistingServerAuth对象激活后被释放- 页面标记中缺失状态相关内容
- Blazor端无已认证用户,身份验证状态未正确传递
部署环境为Orchard Core,怀疑OC存在冲突,以下为相关配置代码:
宿主Startup.ConfigureServices方法
services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); services.AddCascadingAuthenticationState(); services.AddScoped<AuthenticationStateProvider, PersistingAuthenticationStateProvider>(); services.AddSignalR(); services.AddHttpsRedirection(options => { options.HttpsPort = 443; }); services.AddOrchardCms() .AddSetupFeatures("OrchardCore.AutoSetup") .ConfigureServices(services => { services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder(new[] { JwtBearerDefaults.AuthenticationScheme }) .RequireAuthenticatedUser() .Build(); }); services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.SaveToken = true; options.Authority = "https://localhost:4433/"; options.RequireHttpsMetadata = true; options.IncludeErrorDetails = true; options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = "crt_customer_portal", ValidIssuer = "https://localhost:4433/", ClockSkew = TimeSpan.Zero, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("TODO_REPLACE_TODO_REPLACE")) //TODO Replace with real key }; }); }) .Configure((app, routes) => { app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); });
Blazor WASM启动代码
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddAuthorizationCore(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>(); await builder.Build().RunAsync();
PersistingAuthenticationStateProvider代码
public class PersistingAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable { private Task<AuthenticationState>? _authenticationStateTask; private readonly PersistentComponentState _state; private readonly PersistingComponentStateSubscription _subscription; private readonly IdentityOptions _options; public PersistingAuthenticationStateProvider(PersistentComponentState persistentComponentState, IOptions<IdentityOptions> optionsAccessor) { _options = optionsAccessor.Value; _state = persistentComponentState; AuthenticationStateChanged += OnAuthenticationStateChanged; _subscription = _state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); } private async Task OnPersistingAsync() { if (_authenticationStateTask is null) { throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}()."); } var authenticationState = await _authenticationStateTask; var principal = authenticationState.User; if (principal.Identity?.IsAuthenticated == true) { var tenantId = principal.FindFirst("TenantId")?.Value; var userId = principal.FindFirst(_options.ClaimsIdentity.UserIdClaimType)?.Value; var name = principal.FindFirst("name")?.Value; var bearerToken = principal.FindFirst("bearer")?.Value; if (userId != null && name != null && bearerToken != null) { _state.PersistAsJson(nameof(UserClaims), new UserClaims { TenantId = tenantId, UserId = userId, Name = name, BearerToken = bearerToken }); } } } private void OnAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask) { _authenticationStateTask = authenticationStateTask; } public void Dispose() { _authenticationStateTask?.Dispose(); AuthenticationStateChanged -= OnAuthenticationStateChanged; _subscription.Dispose(); } }
1. Orchard Core服务上下文冲突
Orchard Core会独立管理自身的服务容器与中间件管道,你当前将Blazor的认证相关服务注册在AddOrchardCms()外部,导致PersistingServerAuthenticationStateProvider的作用域被Orchard的容器覆盖,无法正常参与状态持久化流程。
解决方法:将Blazor的服务注册移至AddOrchardCms().ConfigureServices()内部,确保与Orchard的服务处于同一配置上下文:
services.AddOrchardCms() .AddSetupFeatures("OrchardCore.AutoSetup") .ConfigureServices(services => { // 移动Blazor服务注册到此处 services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); services.AddCascadingAuthenticationState(); services.AddScoped<AuthenticationStateProvider, PersistingAuthenticationStateProvider>(); services.AddSignalR(); // 原有授权、认证配置... })
2. Bearer Token未存入Claims
JWT认证默认不会将原始Token添加到Claims集合中,你的PersistingAuthenticationStateProvider尝试读取bearer类型的Claim会返回null,导致状态无法持久化。
解决方法:在JWT配置中添加OnTokenValidated事件,将原始Token注入Claims:
.AddJwtBearer(options => { options.SaveToken = true; // 其他原有配置... options.Events = new JwtBearerEvents { OnTokenValidated = context => { var jwtToken = context.SecurityToken as JwtSecurityToken; if (jwtToken != null) { var identity = context.Principal.Identity as ClaimsIdentity; identity?.AddClaim(new Claim("bearer", jwtToken.RawData)); } return Task.CompletedTask; } }; });
3. 持久化逻辑的渲染模式限制
你注册RegisterOnPersisting时指定了RenderMode.InteractiveWebAssembly,如果页面中的Blazor组件使用Auto或Server渲染模式,会导致持久化逻辑不触发。
解决方法:移除渲染模式限制,确保所有模式下都能执行状态持久化:
_subscription = _state.RegisterOnPersisting(OnPersistingAsync);
4. WASM端未加载持久化状态
当前WASM端仅注册了PersistentAuthenticationStateProvider,但未实现从页面的PersistentComponentState中读取并还原认证状态的逻辑,导致Blazor端无法获取已认证用户信息。
解决方法:修改WASM端的PersistentAuthenticationStateProvider,添加状态读取逻辑:
public class PersistentAuthenticationStateProvider : AuthenticationStateProvider { private readonly Task<AuthenticationState> _initialState; public PersistentAuthenticationStateProvider(PersistentComponentState state) { if (!state.TryTakeFromJson<UserClaims>(nameof(UserClaims), out var userClaims) || userClaims == null) { _initialState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); return; } var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, userClaims.UserId), new Claim(ClaimTypes.Name, userClaims.Name), new Claim("bearer", userClaims.BearerToken) }; if (!string.IsNullOrEmpty(userClaims.TenantId)) { claims.Add(new Claim("TenantId", userClaims.TenantId)); } var identity = new ClaimsIdentity(claims, "Bearer"); _initialState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity))); } public override Task<AuthenticationState> GetAuthenticationStateAsync() { return _initialState; } }
5. 页面组件渲染模式验证
确保宿主页面中嵌入Blazor组件时使用正确的渲染模式,触发状态传递流程:
<component type="typeof(App)" render-mode="InteractiveWebAssembly" />
内容的提问来源于stack exchange,提问作者Will Comeaux

