建议将角色声明作为权限的最佳做法

Pac*_*der 6 permissions oauth-2.0 jwt asp.net-identity .net-core

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

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

我们决定使用现成的AspNetRoleClaims表将用户的声明存储为权限。每个用户都被分配了1个主要角色,尽管潜在的角色是多个角色。每个角色将有许多声明-存储在AspNetRoleClaims表中。

角色声明如下所示:

ClaimType:权限

ClaimValue(s):

MyModule1.Create

MyModule1.Read

MyModule1。编辑

MyModule1.Delete

MyModule1.SomeOtherPermission

MyModule2.Read

MyModule3.Read

MyModule3。编辑

等等

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

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

我正在寻找有关使用角色声明进行承载身份验证的“最佳实践”的建议。Microsoft为您提供了适用于我的方案的开箱即用的AspNetRoleClaims,据我所知,将这些角色声明存储在access_token中的优点是我们不必在受自定义策略保护的每个API终结点上访问数据库。

从我的角度来看,我可以尝试使声明值变小,并且在用户具有多个角色且可能共享重复的常见角色声明的情况下,我可以尝试在将这些声明写入Cookie和删除重复项。

但是,由于该应用程序仍在开发中,因此可以预见会有越来越多的角色声明被添加,并且由于Cookie和access_token的存在,HTTP标头始终可能变得太大。不知道这是否是最好的方法。

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

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

无论如何,只要针对这种情况寻找有关最佳实践建议的反馈和建议。

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

Pac*_*der 7

我从未找到关于如何完成此操作的推荐“最佳实践”,但是由于有一些有用的博客文章,我能够为我从事的项目设计一个不错的解决方案。我决定从id令牌和Identity cookie中排除身份声明,并进行每个请求检查用户权限(角色声明)服务器端的工作。

我最终使用了上面描述的体系结构,使用了内置的AspNetRoleClaims表,并使用给定角色的权限填充该表。

例如:

ClaimType:权限

ClaimValue(s):

MyModule1.Create

MyModule1.Read

MyModule1。编辑

MyModule1.Delete

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

我也有一个枚举类,其中所有权限都存储为枚举。这个枚举使我无需使用魔术字符串即可引用代码中的权限。

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

}
Run Code Online (Sandbox Code Playgroud)

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

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)));
        }
Run Code Online (Sandbox Code Playgroud)

因此,存在一个匹配的Permission和一个PermissionRequirement,如下所示:

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);
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

权限GetEnumDescription上的扩展方法只是将我在代码中具有的枚举用于每个权限,并将其转换为与存储在数据库中相同的字符串名称。

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();
    }
Run Code Online (Sandbox Code Playgroud)

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我为用户建立了一系列不同的角色声明-我使用ClaimsComparer类来帮助实现这一点。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

现在这里是重要的部分-您需要重写UserClaimsPrincipalFactory,以防止将角色声明填充到Identity cookie中。这解决了Cookie和标头太大的问题。感谢Ben Foster的有用帖子(请参见下面的链接)

这是我的自定义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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在Startup.cs中注册此类

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

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

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

AspNet身份角色声明

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

该解决方案在我正在进行的项目中效果很好-希望它可以帮助其他人。

  • 感谢您抽出时间回到这个问题并提供一个很好的全面更新。很感激,我相信我可以从中得到一些有用的东西。 (2认同)