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

来说说 RBAC (role based access control) 

这是目前全世界最通用的权限管理机制, 当然使用率高并不是说它最好. 它也有很多局限的. 

我们来讲讲最简单的 role based 概念. 

想象一间公司开始的时候,只有一个管理人. 

一个 application 一个 user, 只要登入就可以了. 

后来业务大了,就要请人来分工,我们不可能简单的开多一个 user 给新人,因为新人只需要做一部分的工作而且外人是不可靠的. 

所以我们需要有限权. 

一般上的做法是从 task 出发. 我们把所有工作切分成多个 task (比如,管理订单, 管理产品, 管理订货, 管理钱..等等)

一般上一个 task 会有一个对应的菜单和 ui 操作页面, 页面里就涉及多个表的 CRUD 等等操作了。

分工的概念就是, 一个 user 可以负责多个 task. 比如我要你去管订单和订货。

但是任务多起来就不好管理了,所以我们又再往上创建一个角色或者职位的概念. 可能是叫经理, 客服, 采购员等等. 

最后变成了 : 一个 user 对应多个 role 对应多个 task 对应多个 operation 

上面只是一个基本概念, 核心思想就是角色与分工. 

很多人也对上面的方式进行了扩展,比如角色继承 (老板用于所有经理得权限) 

再比如角色组, 我可以把权限设置给组, 那么里头得角色都拥有这个权限了.

这些扩展都是为了更方便得去收发权限. 

动态创建角色 ?  

有些系统可以让用户自己去创建角色, 然后配置给用户. 

看上去很厉害, 但其实所谓的动态都必须满足一个前提,那就是分工已经设定好了. 

如果我提前已经把所有业务切分成一个一个 task, 那么自然可以让用户去自定义角色, 然后角色选择匹配的 task 就好了. 

一人粉饰多角 ? 

如果我们想给一个 user 一些 task, 一个方式是,我把 task 添加到他现有的 role 里.

另外一个做法是添加一个 role 给这个 user.

2 个的结果是一样的,但是以管理的角度看却不同. 我不太喜欢一人多角色. 

这往往容易造成混乱. 但是有时是无法避免的. 

举例 : 

一间补习中心, 一位老师也可能同时是一位家长. 

如果我们有一个 view report 的页面. 按理说, 老师可以观看她学生的 report,而家长可以观看他孩子的 report.

对于这个 user, 我们就很纳闷了, 难不成学生和孩子都出现给他看 ? 然后在标记一个符号 ?

常见的方法是 

1. 让用户选择 role 来访问. 如果 user 选择以家长身份, 那么就呈现孩子, 如果是老师就显示学生 

2. 做多一个菜单, 一个是看学生 report ,一个是看孩子 report. 

本质上都是区别了 2 者的对待. 

rbac 的不足 

权限比我们想象的复杂, 大部分情况下,我们是通过拦截用户的 http 请求来限制用户的访问. 

也就是说,要嘛可以,要嘛不可以. 

但现实情况下还有很多一半一半的情况. 比如, 你是销售经理, 但是是负责 A 区, 所以你只可以看见 A 区的客户资料和销售报告. 

这个在代码里,是要通过 sql where 来实现的. 根本不是简单的能访问 api 还是不行. 

另外, 如果我们只记入 role = 销售经理,那么我们要在哪里记入这个 user 属于哪个区域呢 ? 

难不成我们做一个 A区销售经理的角色 ? 然后开一个 api 专门负责这个请求 ? 

这显然是不合理的. 

所以就有了一个新的概念叫 abac (attribute base access control) 

这个下一篇说, 现在我们来看看 identity 如果帮助我们实现 rbac 吧. 

首先是修改 startup service

// 替换
//services.AddAuthentication(o =>
//{
//    o.DefaultScheme = IdentityConstants.ApplicationScheme;
//    o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
//})
//.AddIdentityCookies(o => { });
//services.AddIdentityCore<IdentityUser>(o =>
//{
//    o.Stores.MaxLengthForKeys = 128;
//})

services.AddIdentity<IdentityUser, IdentityRole>(o =>
{
    o.Stores.MaxLengthForKeys = 128;
})
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddRoles<IdentityRole>(); // 加这个

我们把 AddIdentityCore 换掉了, 加了一个 AddRoles 

这里来看一看源码 AddDefaultIdentity vs AddIdentityCore  vs AddIdentity 

AddDefaultIdentity

AddIdentityCore  

AddIdentity 

public static IdentityBuilder AddIdentity<TUser, TRole>(
    this IServiceCollection services,
    Action<IdentityOptions> setupAction)
    where TUser : class
    where TRole : class
{
    // Services used by identity
    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);
    });

    // Hosting doesn't add IHttpContextAccessor by default
    services.AddHttpContextAccessor();
    // Identity services
    services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
    services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
    services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
    services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
    services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
    // No interface for the error describer so we can add errors without rev'ing the interface
    services.TryAddScoped<IdentityErrorDescriber>();
    services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
    services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
    services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
    services.TryAddScoped<UserManager<TUser>>();
    services.TryAddScoped<SignInManager<TUser>>();
    services.TryAddScoped<RoleManager<TRole>>();

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

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

要完整的话,还是 addIdentity 比较齐全. 

来看看数据结构 

identity 的结构是这样的, 一个 user 对应多个 role 对应多个 claim 

上面我们说到 "最后变成了 : 一个 user 对应多个 role 对应多个 task 对应多个 operation" 

这和我们上面说的不太对应啊. 没错. 是对不上的.

[Authorize(Roles = "Admin")]
public class ContactModel : PageModel

这个是 identity 教程中如何使用 role 来保护页面的做法. 

而我上面说的应该是这样的表达才对

[Authorize(Task = "ManageContact")]
public class ContactModel : PageModel

操作基于 task 来区分, role 绑定 task, 这样才能灵活替换 role 和 task. 依据 identity 的做法, 如果我想把某个工作分给另一个 role 就得修改代码了。

那 claim 又时啥呢 ? claim 就是 abac 的属性. 我们下一篇要讲的东西先不管.

猜你喜欢

转载自www.cnblogs.com/keatkeat/p/10789693.html