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

时间:2017-03-02 18:02:00

标签: permissions oauth-2.0 asp.net-identity jwt .net-core

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

Custom Policy Based Authentication

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

角色声明如下:

ClaimType:权限

ClaimValue(S):

MyModule1.Create

MyModule1.Read

MyModule1.Edit

MyModule1.Delete

MyModule1.SomeOtherPermission

MyModule2.Read

MyModule3.Read

MyModule3.Edit

用户拥有的权限或角色声明越多,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完成更细粒度的权限方案的示例。这一定是我认为相当普遍的要求......

无论如何,只是为这样的场景寻找建议的最佳实践的一些反馈和建议。

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

1 个答案:

答案 0 :(得分:4)

我从来没有找到关于如何实现这一目标的推荐“最佳实践”,但是由于一些有用的博客文章,我能够为我正在开发的项目构建一个很好的解决方案。我决定从id令牌和Identity cookie中排除身份声明,并在每次请求时检查用户权限(角色声明)服务器端。

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

例如:

ClaimType:权限

ClaimValue(S):

MyModule1.Create

MyModule1.Read

MyModule1.Edit

MyModule1.Delete

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

我还有一个枚举类,它将所有权限存储为枚举。这个枚举只是让我参考代码中的权限,而不必使用魔术字符串。

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

}

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

所以有一个匹配的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);
        }

    }
}

权限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();
    }

我的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;
    }
}

我为用户构建了一个不同角色声明的列表 - 我使用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;
    }
}

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

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

现在,这是重要的部分 - 您需要覆盖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);
    }
}

在Startup.cs中注册此课程

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

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

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

AspNet Identity Role Claims

Customizing claims transformation in AspNet Core Identity

这个解决方案对我正在进行的项目运作良好 - 希望它可以帮助其他人。