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

Blazor WebAssembly角色声明拆分失效问题求助

问题:Blazor WebAssembly多角色声明未被正确拆分

我正在开发Blazor WebAssembly客户端项目,已在Program.cs中配置相关服务,并实现CustomUserFactory类用于将数组形式的声明拆分为多个独立声明。但Token中包含的多角色声明("http://schemas.microsoft.com/ws/2008/06/identity/claims/role": ["SuperUtilisateur","Administrateur","Utilisateur"])未被正确拆分,导致多角色功能无法正常工作。

Program.cs 代码

using Blazored.LocalStorage;
using Epicerie_Client;
using Epicerie_Client.Services;
using Epicerie_Client.Services.Interfaces;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddApiAuthorization()
    .AddAccountClaimsPrincipalFactory<CustomUserFactory>();

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl")) });

builder.Services.AddScoped<IDepartementService, DepartementService>();
builder.Services.AddScoped<IItemService, ItemService>();
builder.Services.AddScoped<IUniteMesureService, UniteMesureService>();
builder.Services.AddScoped<IGabaritService, GabaritService>();
builder.Services.AddScoped<IEpicerieService, EpicerieService>();
builder.Services.AddScoped<IEpicerieDetailsService, EpicerieDetailsService>();
builder.Services.AddScoped<IGabaritDetailsService, GabaritDetailsService>();

builder.Services.AddBlazoredLocalStorage();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();

await builder.Build().RunAsync();

CustomUserFactory 代码

public class CustomUserFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
    public CustomUserFactory(IAccessTokenProviderAccessor accessor)
        : base(accessor)
    {
    }

    public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var user = await base.CreateUserAsync(account, options);
        var claimsIdentity = (ClaimsIdentity)user.Identity;

        if (account != null)
        {
            MapArrayClaimsToMultipleSeparateClaims(account, claimsIdentity);
        }

        return user;
    }

    private void MapArrayClaimsToMultipleSeparateClaims(RemoteUserAccount account, ClaimsIdentity claimsIdentity)
    {
        foreach (var prop in account.AdditionalProperties)
        {
            var key = prop.Key;
            var value = prop.Value;
            if (value != null &&
                (value is JsonElement element && element.ValueKind == JsonValueKind.Array))
            {
                claimsIdentity.RemoveClaim(claimsIdentity.FindFirst(prop.Key));
                var claims = element.EnumerateArray()
                    .Select(x => new Claim(prop.Key, x.ToString()));
                claimsIdentity.AddClaims(claims);
            }
        }
    }
}

问题分析

1. 自定义AuthenticationStateProvider覆盖了默认提供器

你在调用AddApiAuthorization().AddAccountClaimsPrincipalFactory<CustomUserFactory>()后,又注册了自己的AuthStateProvider作为AuthenticationStateProvider的实现:

builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();

这会替换掉AddApiAuthorization自动注册的RemoteAuthenticationService(它依赖AccountClaimsPrincipalFactory),导致你的CustomUserFactory从未被执行,数组声明自然不会被拆分。

2. 角色声明的键可能被映射或提前处理

base.CreateUserAsync方法会自动将Token中的声明映射到ClaimsPrincipal,对于数组形式的role声明,默认处理逻辑可能会将其转换为一个包含逗号分隔值的单个Claim,此时account.AdditionalProperties中可能已经不存在原始的数组键,或者键被替换为ClaimTypes.Role(即http://schemas.microsoft.com/ws/2008/06/identity/claims/role的常量别名)。


解决方案

方案1:移除自定义AuthenticationStateProvider(如果不需要)

如果你的AuthStateProvider没有特殊的自定义逻辑,可以直接删除这行注册代码,让AddApiAuthorization提供的默认实现生效,这样CustomUserFactory会被正常调用:

// 移除这两行
// builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
// builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();

方案2:让自定义AuthStateProvider使用CustomUserFactory

如果你需要保留自定义的AuthStateProvider,需要在其中手动调用CustomUserFactory来处理声明,或者确保你的AuthStateProvider继承自RemoteAuthenticationService并使用注册的工厂。

修改AuthStateProvider的实现,注入CustomUserFactory并在获取用户时调用它:

public class AuthStateProvider : AuthenticationStateProvider
{
    private readonly CustomUserFactory _userFactory;
    // 其他依赖注入

    public AuthStateProvider(CustomUserFactory userFactory, /* 其他服务 */)
    {
        _userFactory = userFactory;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        // 你的现有逻辑,获取RemoteUserAccount或用户信息
        RemoteUserAccount account = /* 从Token或存储中获取用户数据 */;
        var user = await _userFactory.CreateUserAsync(account, new RemoteAuthenticationUserOptions());
        return new AuthenticationState(user);
    }
}

方案3:优化CustomUserFactory的处理逻辑

确保能正确识别角色声明的键,即使它被映射过:

private void MapArrayClaimsToMultipleSeparateClaims(RemoteUserAccount account, ClaimsIdentity claimsIdentity)
{
    // 同时检查原始键和ClaimTypes.Role
    var roleClaimTypes = new[] { 
        "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
        ClaimTypes.Role
    };

    foreach (var prop in account.AdditionalProperties)
    {
        var key = prop.Key;
        var value = prop.Value;
        
        // 如果是角色类型的键,强制处理数组
        if (roleClaimTypes.Contains(key) && value != null && value is JsonElement element && element.ValueKind == JsonValueKind.Array)
        {
            // 先移除所有已存在的同类型声明
            var existingClaims = claimsIdentity.FindAll(key).ToList();
            foreach (var claim in existingClaims)
            {
                claimsIdentity.RemoveClaim(claim);
            }
            
            // 添加拆分后的角色声明
            var claims = element.EnumerateArray()
                .Select(x => new Claim(ClaimTypes.Role, x.ToString()));
            claimsIdentity.AddClaims(claims);
        }
        // 处理其他数组声明
        else if (value != null && value is JsonElement elem && elem.ValueKind == JsonValueKind.Array)
        {
            var existingClaim = claimsIdentity.FindFirst(key);
            if (existingClaim != null)
            {
                claimsIdentity.RemoveClaim(existingClaim);
            }
            var claims = elem.EnumerateArray()
                .Select(x => new Claim(key, x.ToString()));
            claimsIdentity.AddClaims(claims);
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 05:51:02