澄清身份授权:将声明用作角色、角色和声明或角色声明

Posted

技术标签:

【中文标题】澄清身份授权:将声明用作角色、角色和声明或角色声明【英文标题】:Clarifying Identity Authorization: using Claims as Roles, Roles and Claims or Role Claims 【发布时间】:2018-02-10 08:00:18 【问题描述】:

我从 ASP.NET Identity 的 Claim 授权开始,如果我需要在我的应用程序中使用“角色”概念,我想澄清如何处理它们。

注意:我对此很陌生,所以所有的概念都在我脑海中浮现,请善待,我们将非常感谢您对任何概念进行进一步的澄清/更正。

1.- 假设我需要管理员和用户角色的“角色”概念,所以我的第一个想法是向ApplicationUsers 添加声明,例如:

user.Claims.Add(new IdentityUserClaim<string>  ClaimType = "Role", ClaimValue = "Admin" );

*其中“用户”是ApplicationUser

但后来我读到它已经由框架完成,因为它有一些预定义的声明类型,所以上面的代码可能是:

user.Claims.Add(new IdentityUserClaim<string>  ClaimType = ClaimTypes.Role, ClaimValue = "Admin" );

这种方法正确吗?或者我应该使用“旧”角色概念并向用户添加一个角色,例如:

await _roleManager.CreateAsync(new IdentityRole("Admin"));    
await _userManager.AddToRoleAsync(user, "Admin");

2.- 现在假设我将角色定义为声明,我如何检查它们的授权?我的意思是,它会起作用吗?

[Authorize(Roles = "Admin")]

或者我应该包含一个政策声明来检查角色声明?

/* In startup ConfigureServices method*/
options.AddPolicy("IsAdmin", policy => 
                policy.RequireClaim(ClaimTypes.Role, "Admin");
                );

...

/*In a controller class*/
[Authorize(Policy = "IsAdmin")]
<controller here>

3.- 现在,存储我的自定义声明的正确方法是什么?我的意思是,ASP.NET 的 ClaimTypes 类只是一堆 const string 值,所有关于 Claims 的示例代码都将它们存储在类似的类中,例如:

public static class ClaimData

    public static List<string> AdminClaims  get; set;  = new List<string>
                                                        
                                                            "Add User",
                                                            "Edit User",
                                                            "Delete User"
                                                        ;

这样好吗?

最后一点。- 我还在互联网上看到了“角色声明”的概念,这篇博文对此进行了解释:http://benfoster.io/blog/asp-net-identity-role-claims

那是什么?如果我还不够困惑,现在还有第三种授权用户的方法。将角色用作声明是否更好?

【问题讨论】:

【参考方案1】:

您描述的方法似乎是正确的。一切都取决于您的要求。

假设您的应用程序中有多个功能,如果您选择使用角色,则属于该功能的代码必须每次检查用户是否处于一组特定的角色中才能使用该功能。当功能和角色增长时,这种方法变得非常难以管理,因为您必须考虑将角色组合到每个单独的功能中。在本例中,只有PowerUserAdministrator的用户才能进行管理操作X。现在,这看起来简单明了,但是,如果您添加一个新角色ALittleBitMorePowerful,它也可以执行X 操作的User,会发生什么情况。为了达到这个结果,您必须审查所有内容并更改检查(这意味着重新测试整个事情)。

如果您使用声明 CanPerformX 设计了功能 X,那么您将引入一个抽象层:您的代码将不关心用户的角色,而只会检查自己的声明。如果您曾经修改声明与用户的关联方式,您的有效代码将不会改变(这最终意味着没有引入正式的回归)。

角色被设计为广泛,而声明被设计为细粒度。但是,正如您在链接中看到的那样,您可能会认为一个角色是“大角色”,或者是一个“小角色”。

我发布了我的代码的一小段摘录,它支持自定义角色但固定声明。 定义声明

    internal static class PolicyClaims
    
        public const string AdministratorClaim = @"http://myorganization/2019/administrator";
        public const string Operation1Claim = @"http://myorganization/2019/op1";
        public const string Operation2Claim = @"http://myorganization/2019/op2";
        public const string ObtainedClaim = @"true";
    

定义策略

    internal static class Policies
    
        public const string RequireAdministrator = "RequireAdministrator";
        public const string RequireOp1 = "RequireOp1";
        public const string RequireOp2 = "RequireOp2";

        public const string AlwaysDeny = "AlwaysDeny";

        public static void ConfigurePolicies(IServiceCollection services)
        
            services.AddAuthorization(options => options.AddPolicy(RequireAdministrator, policy => policy.RequireClaim(PolicyClaims.AdministratorClaim)));
            services.AddAuthorization(options => options.AddPolicy(RequireOp1, policy => policy.RequireClaim(PolicyClaims.Operation1Claim)));
            services.AddAuthorization(options => options.AddPolicy(RequireOp2, policy => policy.RequireClaim(PolicyClaims.Operation2Claim)));
            services.AddAuthorization(options => options.AddPolicy(AlwaysDeny, policy => policy.RequireUserName("THIS$USER\n\r\t\0cannot be created")));
        
    

Startup.RegisterServices注册政策

    Policies.ConfigurePolicies(services);

在您对用户进行身份验证的位置,根据您的逻辑决定需要添加哪些声明(省略了一些部分以专注于概念)

    [AllowAnonymous]
    [Route("api/authentication/authenticate")]
    [HttpPost()]
    public async Task<IActionResult> Authenticate([FromBody] LoginModel model)
    
        if (ModelState.IsValid)
        
            var user = m_UserManager.Users.FirstOrDefault(x => x.UserName == model.UserName);

            if (user == null)
            
                ...
            
            else
            
                var result = await m_SignInManager.CheckPasswordSignInAsync(user, model.Password, false);
                if (result.Succeeded)
                
                    var handler = new JwtSecurityTokenHandler();
                    var tokenDescriptor = new SecurityTokenDescriptor
                    
                        Subject = new ClaimsIdentity(new Claim[]
                        
                        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                        new Claim(ClaimTypes.Name, model.UserName)
                        ),
                        Expires = DateTime.UtcNow.AddHours(2),
                        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(InstanceSettings.JWTKey), SecurityAlgorithms.HmacSha256Signature)
                    ;

                    var roles = await m_UserManager.GetRolesAsync(user);

                    AddClaims(tokenDescriptor, roles);

                    var token = handler.CreateToken(tokenDescriptor);
                    var tokenString = handler.WriteToken(token);

                    return ...
                
                else
                
                    ...
                
            
        
        return ...
    

    private static void AddClaims(SecurityTokenDescriptor tokenDescriptor, IList<string> roles)
    
        if (roles.Any(x => string.Equals(Constants.AdministratorRoleName, x, StringComparison.OrdinalIgnoreCase)))
        
            tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.AdministratorClaim, PolicyClaims.ObtainedClaim));

            tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.Operation1Claim, PolicyClaims.ObtainedClaim));
            tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.Operation2Claim, PolicyClaims.ObtainedClaim));
        
        ... query the database and add each claim with value PolicyClaims.ObtainedClaim ...
    

最后,您可以使用这些策略来保护您的代码:

    [Authorize(Policy = Policies.RequireAdministrator)]
    [HttpPost("execute")]
    public async Task<IActionResult> ExecuteOperation([FromBody] CommandModel model)
    
        ...
    

请注意,在这种方法中,我将某些声明硬编码给管理员,因为我想阻止管理员删除某些声明。但是,这不是强制性的。

【讨论】:

您如何推断不使用声明、角色或策略,而只是从令牌中获取用户 ID 并手动检查数据库的权限? 目前我无法提供任何代码:您可以将用户 ID 存储在令牌中(实际上,如果您使用 JWT,则您已经拥有相应的工具来处理该问题)以及答案中的代码。然后,您必须在 api 控制器中禁用任何令牌强制执行,从标头中获取原始令牌(类似于 var accessToken = Request.Headers[HeaderNames.Authorization];),手动解析令牌,提取用户 ID,执行查找,然后就完成了。它很复杂,您正在混合基础设施代码和业务代码。我不会推荐它。查看其他评论。 您可以通过使用答案上发布的代码来获取您所要求的内容,并定义一个策略,如果需要特定声明,只需获取用户 ID 声明的值(或您想要的)然后使用您的条件执行查找。该政策应适用于任何地方。恕我直言,这种方法使用推荐的模式,并允许您以更清晰的方式分离关注点。 啊,我完全同意。我什至asked specifically 关于它。我的一位同事深信我们应该按照习惯的方式来做,我不敢放下脚,以防万一我错过了什么。但是现在,我已经从三个不同的来源得到了确认,所以我更放心了。如果您认为这对读者有益,欢迎在链接的问题中投入 2 美分。

以上是关于澄清身份授权:将声明用作角色、角色和声明或角色声明的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core 身份中的角色声明与自定义身份验证中的角色权限相比

Blazor WebAssembly .Net 5 Msal 身份验证中基于角色的授权

将身份角色声明 Pk 类型更改为 Guid

ASP.NET Core 身份角色、声明和用户

使用 OpenId Connect 进行基于声明的身份验证

使用 JWT 的角色