将角色声明作为权限的推荐最佳实践 [英] Recommended best practice for role claims as permissions

查看:25
本文介绍了将角色声明作为权限的推荐最佳实践的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在开发的应用程序是一个 SPA,我们在与使用 .NETCore 和 ASP.NET Identity 的后端 API 通信时使用 JWT Bearer 身份验证和 OpenIdConnect/OAuth2.我们的 API 端点使用基于自定义策略的身份验证进行保护,如下所示:

The app I am working on is a SPA and we are using JWT Bearer authentication and OpenIdConnect/OAuth2 when communicating with our backend API which uses .NETCore and ASP.NET Identity. Our API endpoints are secured using Custom Policy based authentication as shown here:

基于自定义策略的身份验证

我们决定使用开箱即用的 AspNetRoleClaims 表来存储我们用户的声明作为权限.尽管可能有多个角色,但每个用户都被分配了 1 个主要角色.每个角色都有许多声明 - 这些声明存储在 AspNetRoleClaims 表中.

We decided to use the out of the box AspNetRoleClaims table to store claims for our users as permissions. Each user is assigned 1 primary role although the potential is there to have multiple roles. Each role will have many claims - which are stored in the AspNetRoleClaims table.

角色声明如下所示:

声明类型:权限

索赔值:

MyModule1.Create

MyModule1.Create

MyModule1.Read

MyModule1.Read

MyModule1.Edit

MyModule1.Edit

MyModule1.Delete

MyModule1.Delete

MyModule1.SomeOtherPermission

MyModule1.SomeOtherPermission

MyModule2.Read

MyModule2.Read

MyModule3.Read

MyModule3.Read

MyModule3.Edit

MyModule3.Edit

用户拥有的权限或角色声明越多,access_token 将越大,从而增加 HTTP 标头大小.还有 ASP.NET 身份授权 cookie - 随着越来越多的角色声明,它被分成多个 cookie.

The more permissions or role claims that a user has, the larger the access_token will be, thereby increasing the HTTP header size. Also the ASP.NET Identity Authorization cookie - as there are more and more role claims it gets chunked out into multiple cookies.

我尝试添加了很多角色声明,但最终请求失败,因为标头太大.

I have experimented with adding in a lot of role claims and eventually the request fails because the header gets too big.

在使用角色声明进行承载身份验证时,我正在寻找一些关于什么被认为是最佳实践"的建议.Microsoft 为您提供适用于我的场景的开箱即用的 AspNetRoleClaims,据我了解,将这些角色声明存储在 access_token 中的优势在于我们不必访问每个 API 端点上的数据库,这些端点使用自定义策略进行保护.

I am looking for some advice on what is considered "best practice" when it comes to bearer authentication with role claims. Microsoft gives you AspNetRoleClaims out of the box that work for my scenario and from what I understand the advantage of storing these role claims in the access_token is that we don't have to hit the database on each API endpoint that is secured with the custom policy.

在我看来,我可以尝试使声明值更小,并且在用户具有多个可能共享重复的公共角色声明的角色的情况下,我可以尝试在这些被写入时进行拦截cookie 并删除重复项.

The way I see it, I can try to make the claim values smaller, and in the case of where a user has multiple roles that may share common role claims that are duplicated, I can try to intercept when these get written into the cookie and remove the duplicates.

但是,由于该应用程序仍在开发中,我可以预见会添加越来越多的角色声明,并且始终存在 HTTP 标头因 cookie 和 access_token 而变得过大的可能性.不确定这是否是最好的方法.

However, since the app is still in development, I can foresee more and more roles claims being added and there is always the possibility that the HTTP header will become too large with the cookies and the access_token. Not sure if this is the best approach.

我看到的唯一替代方法是每次我们访问受保护的 API 时都访问数据库.我可以在每个自定义声明策略要求处理程序中注入一个 DbContext,并在每个请求上与 AspNetRoleClaims 表对话.

The only alternative I see is to hit the database each time we hit our protected API. I could inject a DbContext in each custom claim policy requirement handler and talk to the AspNetRoleClaims table on each request.

我还没有看到太多关于人们如何使用 ASP.NET Identity 和 .NET Core API 实现更细粒度的权限方案的示例.我认为这一定是一个相当普遍的要求......

I haven't seen too many examples out there of how people accomplish a more finely grained permissions scheme with ASP.NET Identity and .NET Core API. This must be a fairly common requirement I would think...

无论如何,只是寻找一些反馈和建议,了解针对此类场景的推荐最佳实践.

Anyways, just looking for some feedback and advice on recommended best practice for a scenario like this.

****更新 - 请参阅下面的答案****

****UPDATE - See answer below ****

推荐答案

我从来没有找到关于如何实现这一点的推荐最佳实践",但多亏了一些有用的博客文章,我能够为该项目构建一个很好的解决方案我在工作.我决定从 id 令牌和身份 cookie 中排除身份声明,并针对每个请求检查用户权限(角色声明)服务器端.

I never did find a recommended "best practice" on how to accomplish this but thanks to some helpful blog posts I was able to architect a nice solution for the project I was working on. I decided to exclude the identity claims from the id token and the Identity cookie and do the work of checking the users permissions (role claims) server side with each request.

我最终使用了上面描述的架构,使用内置的 AspNetRoleClaims 表并使用给定角色的权限填充它.

I ended up using the architecture are described above, using the built in AspNetRoleClaims table and populating it with permissions for a given role.

例如:

声明类型:权限

索赔值:

MyModule1.Create

MyModule1.Create

MyModule1.Read

MyModule1.Read

MyModule1.Edit

MyModule1.Edit

MyModule1.Delete

MyModule1.Delete

我使用基于自定义策略的身份验证,如上面链接中的 Microsoft 文章中所述.然后我使用基于角色的策略锁定我的每个 API 端点.

I use Custom policy based authentication as described in the Microsoft article in the link above. Then I lock down each of my API endpoints with the Role based policy.

我还有一个枚举类,该类将所有权限存储为枚举.这个枚举只是让我在代码中引用权限,而不必使用魔法字符串.

I also have an enum class that has all the permissions stored as enums. This enum just lets me refer to the permission in code without having to use magic strings.

public enum Permission
{
    [Description("MyModule1.Create")]
    MyModule1Create,
    [Description("MyModule1.Read")]
    MyModule1Read,
    [Description("MyModule1.Update")]
    MyModule1Update,
    [Description("MyModule1.Delete")]
    MyModule1Delete
}

我像这样在 Startup.cs 中注册权限:

I register the permissions in Startup.cs like so:

services.AddAuthorization(options =>
        {
            options.AddPolicy("MyModule1Create",
                p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Create)));
            options.AddPolicy("MyModule1Read",
                p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Read)));
            options.AddPolicy("MyModule1Update",
                p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Update)));
            options.AddPolicy("MyModule1Delete",
                p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Delete)));
        }

所以有一个匹配的 Permission 和一个 PermissionRequirement 像这样:

So there is a matching Permission and a PermissionRequirement like so:

public class PermissionRequirement : IAuthorizationRequirement
{
    public PermissionRequirement(Permission permission)
    {
        Permission = permission;
    }

    public Permission Permission { get; set; }
}

public class PermissionRequirementHandler : AuthorizationHandler<PermissionRequirement>,
    IAuthorizationRequirement

{
    private readonly UserManager<User> _userManager;
    private readonly IPermissionsBuilder _permissionsBuilder;

    public PermissionRequirementHandler(UserManager<User> userManager,
        IPermissionsBuilder permissionsBuilder)
    {
        _userManager = userManager;
        _permissionsBuilder = permissionsBuilder;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement)
    {
        if (context.User == null)
        {
            return;
        }

        var user = await _userManager.GetUserAsync(context.User);
        if (user == null)
        {
            return;
        }

        var roleClaims = await _permissionsBuilder.BuildRoleClaims(user);

        if (roleClaims.FirstOrDefault(c => c.Value == requirement.Permission.GetEnumDescription()) != null)
        {
            context.Succeed(requirement);
        }

    }
}

权限 GetEnumDescription 的扩展方法只获取我在代码中为每个权限拥有的枚举,并将其转换为与存储在数据库中相同的字符串名称.

The extension method on the permission GetEnumDescription just takes the enum that I have in the code for each permission and translates it to the same string name as it is stored in the database.

public static string GetEnumDescription(this Enum value)
{
    FieldInfo fi = value.GetType().GetField(value.ToString());

    DescriptionAttribute[] attributes =
        (DescriptionAttribute[])fi.GetCustomAttributes(
        typeof(DescriptionAttribute),
        false);

    if (attributes != null &&
        attributes.Length > 0)
        return attributes[0].Description;
    else
        return value.ToString();
}

我的 PermissionHandler 有一个 PermissionsBuilder 对象.这是我写的一个类,它将访问数据库并检查登录用户是否具有特定的角色声明.

My PermissionHandler has a PermissionsBuilder object. This is a class I wrote that will hit the database and check if the logged in user has a particular role claim.

public class PermissionsBuilder : IPermissionsBuilder
{
    private readonly RoleManager<Role> _roleManager;

    public PermissionsBuilder(UserManager<User> userManager, RoleManager<Role> roleManager)
    {
        UserManager = userManager;
        _roleManager = roleManager;

    }

    public UserManager<User> UserManager { get; }

    public async Task<List<Claim>> BuildRoleClaims(User user)
    {
        var roleClaims = new List<Claim>();
        if (UserManager.SupportsUserRole)
        {
            var roles = await UserManager.GetRolesAsync(user);
            foreach (var roleName in roles)
            {
                if (_roleManager.SupportsRoleClaims)
                {
                    var role = await _roleManager.FindByNameAsync(roleName);
                    if (role != null)
                    {
                        var rc = await _roleManager.GetClaimsAsync(role);
                        roleClaims.AddRange(rc.ToList());
                    }
                }
                roleClaims = roleClaims.Distinct(new ClaimsComparer()).ToList();
            }
        }
        return roleClaims;
    }
}

我为用户建立了一个不同角色声明的列表 - 我使用 ClaimsComparer 类来帮助做到这一点.

I build up a list of distinct role claims for a user - I use a ClaimsComparer class to help do this.

public class ClaimsComparer : IEqualityComparer<Claim>
{
    public bool Equals(Claim x, Claim y)
    {
        return x.Value == y.Value;
    }
    public int GetHashCode(Claim claim)
    {
        var claimValue = claim.Value?.GetHashCode() ?? 0;
        return claimValue;
    }
}

控制器被基于角色的自定义策略锁定:

The controllers are locked down with the role based custom policy:

[HttpGet("{id}")]
[Authorize(Policy = "MyModule1Read", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Get(int id){  

现在是重要的部分 - 您需要覆盖 UserClaimsPrincipalFactory 以防止角色声明被填充到身份 cookie 中.这样就解决了cookie和headers太大的问题.感谢 Ben Foster 的有用帖子(见下面的链接)

Now here is the important part - you need to override the UserClaimsPrincipalFactory in order to prevent the role claims from being populated into the Identity cookie. This solves the problem of the cookie and the headers being too big. Thanks to Ben Foster for his helpful posts (see links below)

这是我的自定义 AppClaimsPrincipalFactory:

Here is my custom AppClaimsPrincipalFactory:

public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<User, Role>
{
    public AppClaimsPrincipalFactory(UserManager<User> userManager, RoleManager<Role> roleManager, IOptions<IdentityOptions> optionsAccessor)
        : base(userManager, roleManager, optionsAccessor)
    {
    }
    public override async Task<ClaimsPrincipal> CreateAsync(User user)
    {
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        var userId = await UserManager.GetUserIdAsync(user);
        var userName = await UserManager.GetUserNameAsync(user);
        var id = new ClaimsIdentity("Identity.Application", 
            Options.ClaimsIdentity.UserNameClaimType,
            Options.ClaimsIdentity.RoleClaimType);
        id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
        id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, userName));
        if (UserManager.SupportsUserSecurityStamp)
        {
            id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
                await UserManager.GetSecurityStampAsync(user)));
        }

        // code removed that adds the role claims 

        if (UserManager.SupportsUserClaim)
        {
            id.AddClaims(await UserManager.GetClaimsAsync(user));
        }

        return new ClaimsPrincipal(id);
    }
}

在 Startup.cs 中注册这个类

Register this class in Startup.cs

services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    // override UserClaimsPrincipalFactory (to remove role claims from cookie )
    services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, AppClaimsPrincipalFactory>();

以下是 Ben Foster 有用博客文章的链接:

Here are the links to Ben Foster's helpful blog posts:

AspNet 身份角色声明

在 AspNet Core Identity 中自定义声明转换

这个解决方案对我正在处理的项目很有效 - 希望它可以帮助其他人.

This solution has worked well for the project I was working on - hope it helps someone else out.

这篇关于将角色声明作为权限的推荐最佳实践的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆