使用MSAL获取令牌后添加角色声明遇权限验证异常问题
我正在为Web API控制器配置角色授权,打算用[Authorize(Roles="ROLE_ADMIN,ROLE_MANAGER")]这类特性装饰控制器。但角色存储在应用数据库的自定义表中,于是我通过以下代码连接数据库并将角色添加到用户声明里——调试时能看到角色已成功附加到Claims,声明格式为http://schemas.microsoft.com/ws/2008/06/identity/claims/role: ROLE_ADMIN,颁发者是LOCAL_AUTHORITY。
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => { options.Events = new JwtBearerEvents { OnTokenValidated = async context => { var claimsIdentity = context.Principal.Identity as ClaimsIdentity; var userEmail = context.Principal.FindFirstValue("preferred_username"); using var dbContext = new SmpDbContext(new DbContextOptionsBuilder<SmpDbContext>() .UseSqlServer(connectionString).Options); var appUser = await dbContext.AppUsers.FirstOrDefaultAsync(u => u.Email == userEmail); if (appUser != null) { var roles = await dbContext.Roles.Where(r => r.Users.Contains(appUser)).ToListAsync(); foreach (var role in roles) { claimsIdentity.AddClaim(new System.Security.Claims.Claim(ClaimTypes.Role, role.Authority)); } } } }; });
但调用API时会触发如下错误,注释掉options.Events代码块后就没有错误了。我怀疑是添加声明的操作导致令牌失效,进而让Graph等Azure服务调用失败。想请教有没有其他在Web API项目中添加角色的方法?我已经能通过builder.Services.AddAuthorization()成功配置策略,但更倾向于用[Authorize(Roles="role1,role2,role3")]而不是[Authorize(Policy="JustThis")]。
Status Code: 0 Microsoft.Graph.ServiceException: Code: generalException Message: An error occurred sending the request. ---> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. ---> MSAL.NetCore.4.55.0.0.MsalUiRequiredException: ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.ApiConfig.Executors.ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken) at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable`1 scopes, String tenantId, MergedOptions mergedOptions, String userFlow, TokenAcquisitionOptions tokenAcquisitionOptions) at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions) StatusCode: 0 ResponseBody: Headers: --- End of inner exception stack trace ---
问题根源
你直接修改ClaimsIdentity的操作破坏了Microsoft Identity Web依赖的原始令牌关联信息,导致MSAL在调用AcquireTokenSilent时找不到对应的用户账户,从而抛出user_null错误。
替代方案
1. 使用ClaimsTransformation(推荐)
这是ASP.NET Core添加自定义声明的标准方式,不会破坏原始身份的完整性:
首先实现IClaimsTransformation接口:
public class CustomClaimsTransformation : IClaimsTransformation { private readonly SmpDbContext _dbContext; public CustomClaimsTransformation(SmpDbContext dbContext) { _dbContext = dbContext; } public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var claimsIdentity = principal.Identity as ClaimsIdentity; if (claimsIdentity == null || !claimsIdentity.IsAuthenticated) { return principal; } var userEmail = principal.FindFirstValue("preferred_username"); if (string.IsNullOrEmpty(userEmail)) { return principal; } var appUser = await _dbContext.AppUsers.FirstOrDefaultAsync(u => u.Email == userEmail); if (appUser != null) { var roles = await _dbContext.Roles.Where(r => r.Users.Contains(appUser)).ToListAsync(); foreach (var role in roles) { claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role.Authority)); } } return principal; } }
然后在Program.cs中注册服务:
// 先注册DbContext builder.Services.AddDbContext<SmpDbContext>(options => options.UseSqlServer(connectionString)); // 注册自定义Claims转换服务 builder.Services.AddScoped<IClaimsTransformation, CustomClaimsTransformation>();
2. 在OnTokenValidated中创建新的ClaimsPrincipal
如果必须在JWT验证事件中处理,不要直接修改原始身份,而是基于原始身份创建新对象:
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => { options.Events = new JwtBearerEvents { OnTokenValidated = async context => { var originalPrincipal = context.Principal; var claimsIdentity = originalPrincipal.Identity as ClaimsIdentity; var userEmail = originalPrincipal.FindFirstValue("preferred_username"); // 通过依赖注入获取DbContext,不要手动实例化 var dbContext = context.HttpContext.RequestServices.GetRequiredService<SmpDbContext>(); var appUser = await dbContext.AppUsers.FirstOrDefaultAsync(u => u.Email == userEmail); if (appUser != null) { var roles = await dbContext.Roles.Where(r => r.Users.Contains(appUser)).ToListAsync(); // 复制原始声明,添加新角色后创建新的Identity var newIdentity = new ClaimsIdentity( claimsIdentity.Claims, claimsIdentity.AuthenticationType, claimsIdentity.NameClaimType, claimsIdentity.RoleClaimType); foreach (var role in roles) { newIdentity.AddClaim(new Claim(ClaimTypes.Role, role.Authority)); } // 替换上下文的Principal context.Principal = new ClaimsPrincipal(newIdentity); } } }; });
3. 配置RoleClaimType(可选)
如果需要统一角色声明的识别规则,可以在身份验证配置中指定:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { RoleClaimType = ClaimTypes.Role // 可替换为自定义声明类型,比如"role" }; });
关键注意事项
- 禁止手动实例化
DbContext,通过依赖注入获取,避免连接池和资源泄漏问题。 - 优先使用
IClaimsTransformation,它是ASP.NET Core的标准扩展点,兼容性更强。 - 修改身份对象时,务必保留原始身份的认证类型、名称声明类型等核心信息,避免破坏MSAL的账户关联逻辑。
内容的提问来源于stack exchange,提问作者Steve Robson

