zl程序教程

您现在的位置是:首页 >  后端

当前栏目

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

2023-09-27 14:23:55 时间

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

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

属性就是 key and value, 表达力非常得强. 

我们可以用 key = role value = "Manager" 来别是 user 的 role 

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

此外如果我们想表达区域可以这样些 

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

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

既然 attribute 如此厉害, 那是不是说,我们不需要 role 了呢. 

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

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 的.