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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 10:44:56