建议将角色声明作为权限的最佳做法 [英] Recommended best practice for role claims as permissions

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

问题描述

我正在使用的应用程序是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.

角色声明如下:

ClaimType:权限

ClaimType: Permission

声明值:

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.

但是,由于该应用程序仍在开发中,因此我可以预见到将添加越来越多的角色声明,并且由于cookie和access_token的存在,HTTP头总是过大.不确定这是否是最好的方法.

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令牌和Identity 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.

例如:

ClaimType:权限

ClaimType: Permission

声明值:

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,以防止将角色声明填充到Identity cookie中.这解决了cookie和标题太大的问题.感谢本·福斯特(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天全站免登陆