Asp.net core Identity + identity server + angular 学习笔记 (第五篇)

ABAC (Attribute Based Access Control) 基于属性得权限管理. html

上回说到了 RBAC 的不足. 那 ABAC 就是用来知足它的. json

属性就是 key and value, 表达力很是得强. api

咱们能够用 key = role value = "Manager" 来别是 user 的 role asp.net

甚至是  { roles : "Manager, Admin" } 来别是多个 roles ide

此外若是咱们想表达区域能够这样些 函数

{ role: "HongKongManager", area : "hongkong" } ui

属性也被称为 claim,上一篇有提到, identity 的结构是 user 对应多个 role 对应多个 claim this

既然 attribute 如此厉害, 那是否是说,咱们不须要 role 了呢. spa

依据 identity 的玩法, role 最终也只是作出了 claim 而已. 因此底层万物都是基于 attribute 来运做的. .net

role 只是由于太通用, identity 才实现了这一上层. 

 

我来讲说目前我本身项目是怎样作管理的. 

首先, 万物基于 task 

这个 task 指的就是某个工做, duty. 好比管理定货,管理人力资源,分析销售报表等等.

要管理定货,必然会须要调用不少 api 接口, 我并不打算把每个接口当成一个 permission.

受权时应该是 base on 老板要员工完成那些任务. 而这个 permission 必然要能够知足全部它须要的 api 接口. 

 

因此若是我要受权一个用户作管理定货,那么它应该要有一个 claim 

属性是 ManageOrder, value 不重要, 能够是 true. 

那么凡是涉及到的接口, 均可以放上这个验证 

[Authorize(Task = "ManageOrder")]

GetOrder()

此外若是咱们要分区,咱们还得加更多的属性。

好比 ManagerOrderArea : "HongKong"

在 GetOrder 里面就要写代码获取这个 claim 而后 Where Area == Claim.Value;

note : 我使用 claim 的作法,是违背了 identity 官网的设计的。我能理解它的用意,可是我也以为个人用法没有错. 

这只是受权管理方式的选择, 

identity 认为在受权时不须要彻底清楚使用令牌的守门员若是去检测令牌. 

好比, 我发给你一个 18 岁的认证, 那不少地方均可以依据这个令牌或者配合其它的条规去实现限制, 好比, 进入夜店,买烟,赌博等等

而在受权的时候,并非直接受权说,你能够进入夜店,你能够买烟,你能够赌博,而只是证实了你 18 岁. 

开车也是同样,受权时只是代表了你拥有驾照,意思时你考试经过了, 而不直接说你能够开车. 

虽然看似逻辑分离了, 但其实它依然有隐藏的关系,好比你弄一个"驾照" 不就是为了查看一我的能不能开车吗 ? 

这种方式的好处就是复用容易. 好比咱们的驾照除了证实了我会开车外,还附带了一些信息, 而有些守门员就能够凭着这些信息来作判断了. 

坏处也是有,在职场里,不少时候咱们是直接表示你是否能够作某件事情,而不是特别搞一个 "执照" 的概念来管理. 

因此我以为在某些场合中,直接表示用户是否能够作某些事是合理的. 

 

上面这种是直接对一个用户受权一个任务. 若是任务不少, 就会很不方便. 

因而咱们就要有 role 了. 一个 role 对应多个 task.

identity 的 role 有一个局限, 就是没法设置更多的 attribute. 

有时候咱们会但愿直接把 Area 定义在 user 或者 role 属性上. 那么无论咱们分发什么任务给它. 

都依据 user Area 或者 Role area 来管理. 这样就很方便. 

 

那下面咱们来 override identity 的 default role. 换上咱们本身要的 pattern 

refer : https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-2.2

public class ApplicationDbContext : IdentityUserContext<User, int, IdentityUserClaim<int>, IdentityUserLogin<int>, IdentityUserToken<int>> // 关键
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {

    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder); // 关键 

        modelBuilder.Entity<User>().HasKey(p => p.Id).ForSqlServerIsClustered(false);
        modelBuilder.Entity<User>().HasIndex(p => p.UserName).IsUnique().ForSqlServerIsClustered(true);
        modelBuilder.Entity<User>().Property(p => p.type).IsRequired().HasMaxLength(128);

        modelBuilder.Entity<UserRole>().HasIndex(p => new { p.name, p.userId }).IsUnique();
        modelBuilder.Entity<UserRole>().HasOne(p => p.user).WithMany(p => p.roles).IsRequired().HasForeignKey(p => p.userId);
        modelBuilder.Entity<UserRole>().Property(p => p.name).IsRequired().HasMaxLength(128);
    }

    public DbSet<UserRole> UserRoles { get; set; }
}

首先是基础 IdentityUserContext, 注意看,它没有 role, 而后是调用 base.OnModelCreating(); 这样 identity 内置的 config 才会跑. (note : 我随便把 Id 变成了 int 而不是默认的 string)

而后是写上咱们自定义的 User and UserRole

public class User: IdentityUser<int>
{
    public string type { get; set; }
    public List<UserRole> roles { get; set; }
}

public class UserRole
{
    public int Id { get; set; }
    public string name { get; set; }
    public int userId { get; set; }
    public User user { get; set; }
}

最后是 startup 

services.AddStoogesIdentity<User>()
    .AddDefaultTokenProviders()
    .AddEntityFrameworkStores<ApplicationDbContext>();

AddStoogesIdentity 代码以下, 是直接从 AddIdentity 源码抄来的, 只是把 Role 的部分清楚掉而已. 

public static class IdentityServiceCollectionExtensions
{
    public static IdentityBuilder AddStoogesIdentity<TUser>(
        this IServiceCollection services)
        where TUser : class
        => services.AddStoogesIdentity<TUser>(setupAction: null);
        
    public static IdentityBuilder AddStoogesIdentity<TUser>(
        this IServiceCollection services,
        Action<IdentityOptions> setupAction)
        where TUser : class
    {
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
            options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
            options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
        })
        .AddCookie(IdentityConstants.ApplicationScheme, o =>
        {
            o.LoginPath = new PathString("/Account/Login");
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
            };
        })
        .AddCookie(IdentityConstants.ExternalScheme, o =>
        {
            o.Cookie.Name = IdentityConstants.ExternalScheme;
            o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
        })
        .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
        {
            o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
            };
        })
        .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
        {
            o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
            o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
        });

        services.AddHttpContextAccessor();
        services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
        services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
        services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
        services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
        services.TryAddScoped<IdentityErrorDescriber>();
        services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
        services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
        services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
        services.TryAddScoped<UserManager<TUser>>();
        services.TryAddScoped<SignInManager<TUser>>();

        if (setupAction != null)
        {
            services.Configure(setupAction);
        }

        return new IdentityBuilder(typeof(TUser), services);
    }
}
View Code

这样就搞定 role 了. 接着咱们要作的 user 登入后, 若是生产 claim. 基本上就是经过 UserRole 配合 role 的属性, user 属性等等去生产一堆的 task claim, task parameter claim 等等

这部分我是用·hardcode 来管理的,由于我接触的项目通常上分工都比较稳定了. 若是你的项目须要让用户管理,也能够设计多一个表来操做. 

 

 

先来讲说 identity policy base 的实现方式 

refer : https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.2

上面说了, identity 在受权时并不直接给出用户能够干什么,而只是表示了用户的一个特性,好比 18岁,是一个经理, 有经过开车训练. 

而后经过 policy 去定义各类权限要求. 

policy 是很抽象的一个词, 里面包含了不少的 requirement, 每个 requirement 都有一个或多个 handler 去判断用户是否符合 requirement. 

若是所有符合就表示经过 policy.

通常作法就是,

定义 requirement 类,

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

 

定义 requirement handler 类. 

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   MinimumAgeRequirement requirement)
    {
// any logic here..
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://contoso.com")) { return Task.CompletedTask; // if no set context.Succeed then is fail } context.Succeed(requirement); // ok return Task.CompletedTask; } }

注册 policy 和 handler 

    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

    services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

在 controller 调用 

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}

若是 policy 太简单,能够直接写函数替代 requirement class and handler class 

    options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));

 

下面来讲说动态 policy 

AuthorizeAttribute 这个东西在 .net framework 也是有的, 它是一个 filter

可是在 asp.net core 它不是 filter, 它只是一个很简单的标签. filter asp.net core 已经作好了 for role and policy 一块儿的

refer : https://www.cnblogs.com/RainingNight/p/authorization-in-asp-net-core.html

而后经过标签, filter 获取了 policy name 而后调用 provider 或者是调用咱们在 startup 注册好的 policy 处理. 

provider 能够动态的生成 policy 的处理而不需在 startup 定义每个 policy 

asp.net core 官方的例子是 

[MinimumAgeAuthorize(21)]

生成 policy name "MinimumAgeAuthorize21" 而后在 provider 咱们会得到这个 name, 而后咱们把 21 parse to int 做为 requirement 的变量.

string parse to int ... 这个操做有一点....可是这就是官方给的实现了. 我看干脆直接输出 json 做为 policy name 那么 provider 想怎么搞均可以了。 

    public class MinimumAgePolicyProvider : DefaultAuthorizationPolicyProvider
    {

        public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
        {

        }

        const string POLICY_PREFIX = "MinimumAge";

        public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
        {
       
if (policyName.Contains("21") && policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) && int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age)) { var policy = new AuthorizationPolicyBuilder(); policy.AddRequirements(new MinimumAgeRequirement(age)); return Task.FromResult(policy.Build()); } return base.GetPolicyAsync(policyName); } }

只能有一个 provider 

services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();

因此若是咱们没有处理完全部的 policy name 那么能够调用 base.GetPolicy 用回 default 的. 

相关文章
相关标签/搜索