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

