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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 02:09:51