.NET 6 Blazor应用中如何在认证后为ClaimsPrincipal添加自定义声明?
在.NET Core Blazor应用中一次性添加自定义声明的实现方案
问题背景
- 已掌握基于声明的授权策略,但官方文档对声明的定义与添加仅提及「创建Identity时由可信方分配声明」
- 需求:从SQL查询获取自定义声明,在用户认证完成后仅一次性嵌入到
ClaimsPrincipal中,避免每次请求都查询数据库,同时兼容API端点和Blazor组件的原生[Authorize]特性 - 尝试过的方案存在问题:
- 中间件方案:每次请求都会执行,不符合「仅一次」的要求
- 自定义
AccountClaimsPrincipalFactory方案:注入HTTP依赖导致应用崩溃,且该方法内执行HTTP调用存在其他生命周期问题
方案1:Blazor Server 场景(推荐)
利用Identity的UserClaimsPrincipalFactory,在用户首次认证时一次性从数据库加载声明,生成的ClaimsPrincipal会被缓存复用。
- 实现自定义
UserClaimsPrincipalFactory
public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser> { private readonly ApplicationDbContext _dbContext; public CustomUserClaimsPrincipalFactory(UserManager<IdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor, ApplicationDbContext dbContext) : base(userManager, optionsAccessor) { _dbContext = dbContext; } public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user) { // 先获取基础身份信息 var principal = await base.CreateAsync(user); var identity = (ClaimsIdentity)principal.Identity; // 从数据库查询自定义声明(替换为你的SQL查询逻辑) var userCustomClaims = await _dbContext.UserCustomClaims .Where(c => c.UserId == user.Id) .ToListAsync(); // 添加自定义声明到身份对象 foreach (var claim in userCustomClaims) { identity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); } return principal; } }
- 在
Program.cs中替换默认工厂
// 注册自定义声明工厂 builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, CustomUserClaimsPrincipalFactory>();
该工厂仅在用户首次登录认证时执行一次,后续请求会复用已生成的ClaimsPrincipal,完全满足「仅一次加载」的需求,且原生[Authorize]特性可直接识别这些声明。
方案2:Blazor WebAssembly 场景
避免在AccountClaimsPrincipalFactory中直接执行HTTP请求,改为在登录成功后一次性获取声明并更新用户身份。
- 实现基础自定义
AccountClaimsPrincipalFactory
public class CustomAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> { public CustomAccountClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor) { } public override async ValueTask<ClaimsPrincipal> CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options) { var principal = await base.CreateUserAsync(account, options); return principal; } }
- 实现自定义
AuthenticationStateProvider(用于更新身份状态)
public class CustomAuthenticationStateProvider : ApiAuthorizationStateProvider { public CustomAuthenticationStateProvider(IAccessTokenProvider accessTokenProvider, NavigationManager navigationManager, ILogger<ApiAuthorizationStateProvider> logger) : base(accessTokenProvider, navigationManager, logger) { } // 提供外部更新身份状态的方法 public void NotifyAuthenticationStateChanged(Task<AuthenticationState> task) { base.NotifyAuthenticationStateChanged(task); } }
- 登录成功后加载并添加声明
在登录页面的回调方法中(比如LoginCompleted事件)执行:
private async Task OnLoginSuccess() { // 调用API获取自定义声明(替换为你的API端点) var httpClient = HttpClientFactory.CreateClient("YourApiClient"); var customClaims = await httpClient.GetFromJsonAsync<List<ClaimDto>>("api/user/customclaims"); // 获取当前用户身份 var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var user = authState.User; var identity = (ClaimsIdentity)user.Identity; // 添加自定义声明 foreach (var claim in customClaims) { identity.AddClaim(new Claim(claim.Type, claim.Value)); } // 更新认证状态,让Blazor组件感知到声明变化 ((CustomAuthenticationStateProvider)AuthenticationStateProvider).NotifyAuthenticationStateChanged( Task.FromResult(new AuthenticationState(user))); }
- 在
Program.cs中注册服务
builder.Services.AddApiAuthorization() .AddAccountClaimsPrincipalFactory<CustomAccountClaimsPrincipalFactory>(); builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
这种方式确保声明仅在登录成功后加载一次,后续请求复用已更新的ClaimsPrincipal,同时规避了CreateUserAsync中执行HTTP请求的生命周期问题。
核心注意事项
- Blazor Server场景下,直接通过数据库查询加载声明的方案最稳定,无HTTP调用风险,且完全符合「仅一次加载」的要求
- Blazor WebAssembly场景下,务必避免在
CreateUserAsync中执行异步操作(如HTTP请求),改为登录后回调处理可有效规避依赖注入和生命周期问题 - 两种方案添加的声明均会被原生
[Authorize]特性(包括策略授权)识别,无需额外修改授权逻辑
内容的提问来源于stack exchange,提问作者Mike
相关产品推荐
相关产品推荐

