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

相關文章
相關標籤/搜索