[Abp vNext 原始碼分析] - 7. 許可權與驗證

myzony發表於2019-08-07

一、簡要說明

在上篇文章裡面,我們在 ApplicationService 當中看到了許可權檢測程式碼,通過注入 IAuthorizationService 就可以實現許可權檢測。不過跳轉到原始碼才發現,這個介面是 ASP.NET Core 原生提供的 “基於策略” 的許可權驗證介面,這就說明 ABP vNext 基於原生的授權驗證框架進行了自定義擴充套件。

讓我們來看一下 Volo.Abp.Ddd.Application 專案的依賴結構(許可權相關)。

[Abp vNext 原始碼分析] - 7. 許可權與驗證

本篇文章下面的內容基本就會圍繞上述框架模組展開,本篇文章通篇較長,因為還涉及到 .NET Core IdentityIdentityServer4 這兩部分。關於這兩部分的內容,我會在本篇文章大概講述 ABP vNext 的實現,關於更加詳細的內容,請查閱官方文件或其他博主的部落格。

二、原始碼分析

ABP vNext 關於許可權驗證和許可權定義的部分,都存放在 Volo.Abp.AuthorizationVolo.Abp.Security 模組內部。原始碼分析我都比較喜歡倒推,即通過實際的使用場景,反向推導 基礎實現,所以後面文章編寫的順序也將會以這種方式進行。

2.1 Security 基礎元件庫

這裡我們先來到 Volo.Abp.Security,因為這個模組程式碼和型別都是最少的。這個專案都沒有模組定義,說明裡面的東西都是定義的一些基礎元件。

[Abp vNext 原始碼分析] - 7. 許可權與驗證

2.1.1 Claims 與 Identity 的快捷訪問

先從第一個擴充套件方法開始,這個擴充套件方法裡面比較簡單,它主要是提供對 ClaimsPrincipalIIdentity 的快捷訪問方法。比如我要從 ClaimsPrincipal / IIdentity 獲取租戶 Id、使用者 Id 等。

public static class AbpClaimsIdentityExtensions
{
    public static Guid? FindUserId([NotNull] this ClaimsPrincipal principal)
    {
        Check.NotNull(principal, nameof(principal));

        // 根據 AbpClaimTypes.UserId 查詢對應的值。
        var userIdOrNull = principal.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.UserId);
        if (userIdOrNull == null || userIdOrNull.Value.IsNullOrWhiteSpace())
        {
            return null;
        }

        // 返回 Guid 物件。
        return Guid.Parse(userIdOrNull.Value);
    }

2.1.2 未授權異常的定義

這個異常我們在老版本 ABP 裡面也見到過,它就是 AbpAuthorizationException 。只要有任何未授權的操作,都會導致該異常被丟擲。後面我們在講解 ASP.NET Core MVC 的時候就會知道,在預設的錯誤碼處理中,針對於程式丟擲的 AbpAuthorizationException ,都會視為 403 或者 401 錯誤。

public class DefaultHttpExceptionStatusCodeFinder : IHttpExceptionStatusCodeFinder, ITransientDependency
{
    // ... 其他程式碼
    
    public virtual HttpStatusCode GetStatusCode(HttpContext httpContext, Exception exception)
    {
        // ... 其他程式碼
        
        // 根據 HTTP 協議對於狀態碼的定義,401 表示的是沒有登入的用於嘗試訪問受保護的資源。而 403 則表示使用者已經登入,但他沒有目標資源的訪問許可權。
        if (exception is AbpAuthorizationException)
        {
            return httpContext.User.Identity.IsAuthenticated
                ? HttpStatusCode.Forbidden
                : HttpStatusCode.Unauthorized;
        }
        
        // ... 其他程式碼
    }
    
    // ... 其他程式碼
}

AbpAuthorizationException 異常來說,它本身並不複雜,只是一個簡單的異常而已。只是因為它的特殊含義,在 ABP vNext 處理異常時都會進行特殊處理。

只是在這裡我說明一下,ABP vNext 將它所有的異常都設定為可序列化的,這裡的可序列化不僅僅是將 Serialzable 標籤打在類上就行了。ABP vNext 還建立了基於 StreamingContext 的建構函式,方便我們後續對序列化操作進行定製化處理。

關於執行時序列化的相關文章,可以參考 《CLR Via C#》第 24 章,我也編寫了相應的 讀書筆記

2.1.3 當前使用者與客戶端

開發人員經常會在各種地方需要獲取當前的使用者資訊,ABP vNext 將當前使用者封裝到 ICurrentUser 與其實現 CurrentUser 當中,使用時只需要注入 ICurrentUser 介面即可。

我們首先康康 ICurrentUser 介面的定義:

public interface ICurrentUser
{
    bool IsAuthenticated { get; }

    [CanBeNull]
    Guid? Id { get; }

    [CanBeNull]
    string UserName { get; }

    [CanBeNull]
    string PhoneNumber { get; }
    
    bool PhoneNumberVerified { get; }

    [CanBeNull]
    string Email { get; }

    bool EmailVerified { get; }

    Guid? TenantId { get; }

    [NotNull]
    string[] Roles { get; }

    [CanBeNull]
    Claim FindClaim(string claimType);

    [NotNull]
    Claim[] FindClaims(string claimType);

    [NotNull]
    Claim[] GetAllClaims();

    bool IsInRole(string roleName);
}

那麼這些值是從哪兒來的呢?從帶有 Claim 返回值的方法來看,肯定就是從 HttpContext.User 或者 Thread.CurrentPrincipal 裡面拿到的。

那麼它的實現就非常簡單了,只需要注入 ABP vNext 為我們提供的 ICurrentPrincipalAccessor 訪問器,我們就能夠拿到這個身份容器(ClaimsPrincipal)。

public class CurrentUser : ICurrentUser, ITransientDependency
{
    // ... 其他程式碼

    public virtual string[] Roles => FindClaims(AbpClaimTypes.Role).Select(c => c.Value).ToArray();

    private readonly ICurrentPrincipalAccessor _principalAccessor;
    
    public CurrentUser(ICurrentPrincipalAccessor principalAccessor)
    {
        _principalAccessor = principalAccessor;
    }
    
    // ... 其他程式碼
    
    public virtual Claim[] FindClaims(string claimType)
    {
        // 直接使用 LINQ 查詢對應的 Type 就能拿到上述資訊。
        return _principalAccessor.Principal?.Claims.Where(c => c.Type == claimType).ToArray() ?? EmptyClaimsArray;
    }
    
    // ... 其他程式碼
}

至於 CurrentUserExtensions 擴充套件類,裡面只是對 ClaimsPrincipal 的搜尋方法進行了多種封裝而已。

PS:

除了 ICurrentUserICurrentClient 之外,在 ABP vNext 裡面還有 ICurrentTenant 來獲取當前租戶資訊。通過這三個元件,取代了老 ABP 框架的 IAbpSession 元件,三個元件都沒有 IAbpSession.Use() 擴充套件方法幫助我們臨時更改當前使用者/租戶。

2.1.4 ClaimsPrincipal 訪問器

關於 ClaimsPrincipal 的內容,可以參考楊總的 《ASP.NET Core 之 Identity 入門》 進行了解,大致來說就是存有 Claim 資訊的聚合物件。

關於 ABP vNext 框架預定義的 Claim Type 都存放在 AbpClaimTypes 型別裡面的,包括租戶 Id、使用者 Id 等資料,這些玩意兒最終會被放在 JWT(JSON Web Token) 裡面去。

一般來說 ClaimsPrincipal 裡面都是從 HttpContext.User 或者 Thread.CurrentPrincipal 得到的,ABP vNext 為我們抽象出了一個快速訪問介面 ICurrentPrincipalAccessor。開發人員注入之後,就可以獲得當前使用者的 ClaimsPrincipal 物件。

public interface ICurrentPrincipalAccessor
{
    ClaimsPrincipal Principal { get; }
}

對於 Thread.CurrentPrincipal 的實現:

public class ThreadCurrentPrincipalAccessor : ICurrentPrincipalAccessor, ISingletonDependency
{
    public virtual ClaimsPrincipal Principal => Thread.CurrentPrincipal as ClaimsPrincipal;
}

而針對於 Http 上下文的實現,則是放在 Volo.Abp.AspNetCore 模組裡面的。

public class HttpContextCurrentPrincipalAccessor : ThreadCurrentPrincipalAccessor
{
    // 如果沒有獲取到資料,則使用 Thread.CurrentPrincipal。
    public override ClaimsPrincipal Principal => _httpContextAccessor.HttpContext?.User ?? base.Principal;

    private readonly IHttpContextAccessor _httpContextAccessor;

    public HttpContextCurrentPrincipalAccessor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
}
擴充套件知識:兩者的區別?

Thread.CurrentPrincipal 可以設定/獲得當前執行緒的 ClaimsPrincipal 資料,而 HttpContext?.User 一般都是被 ASP.NET Core 中介軟體所填充的。

最新的 ASP.NET Core 開發建議是不要使用 Thread.CurrentPrincipalClaimsPrincipal.Current (內部實現還是使用的前者)。這是因為 Thread.CurrentPrincipal 是一個靜態成員...而這個靜態成員在非同步程式碼中會出現各種問題,例如有以下程式碼:

// Create a ClaimsPrincipal and set Thread.CurrentPrincipal
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(ClaimTypes.Name, "User1"));
Thread.CurrentPrincipal = new ClaimsPrincipal(identity);

// Check the current user
Console.WriteLine($"Current user: {Thread.CurrentPrincipal?.Identity.Name}");

// For the method to complete asynchronously
await Task.Yield();

// Check the current user after
Console.WriteLine($"Current user: {Thread.CurrentPrincipal?.Identity.Name}");

await 執行完成之後會產生執行緒切換,這個時候 Thread.CurrentPrincipal 的值就是 null 了,這就會產生不可預料的後果。

如果你還想了解更多資訊,可以參考以下兩篇博文:

2.1.5 字串加密工具

這一套東西就比較簡單了,是 ABP vNext 為我們提供的一套開箱即用元件。開發人員可以使用 IStringEncryptionService 來加密/解密你的字串,預設實現是基於 Rfc2898DeriveBytes 的。關於詳細資訊,你可以閱讀具體的程式碼,這裡不再贅述。

2.2 許可權與校驗

Volo.Abp.Authorization 模組裡面就對許可權進行了具體定義,並且基於 ASP.NET Core Authentication 進行無縫整合。如果讀者對於 ASP.NET Core 認證和授權不太瞭解,可以去學習一下 雨夜朦朧 大神的《ASP.NET Core 認證於授權》系列文章,這裡就不再贅述。

2.2.1 許可權的註冊

在 ABP vNext 框架裡面,所有使用者定義的許可權都是通過繼承 PermissionDefinitionProvider,在其內部進行註冊的。

public abstract class PermissionDefinitionProvider : IPermissionDefinitionProvider, ITransientDependency
{
    public abstract void Define(IPermissionDefinitionContext context);
}

開發人員繼承了這個 Provider 之後,在 Define() 方法裡面就可以註冊自己的許可權了,這裡我以 Blog 模組的簡化 Provider 為例。

public class BloggingPermissionDefinitionProvider : PermissionDefinitionProvider
{
    public override void Define(IPermissionDefinitionContext context)
    {
        var bloggingGroup = context.AddGroup(BloggingPermissions.GroupName, L("Permission:Blogging"));

                // ... 其他程式碼。
                
        var tags = bloggingGroup.AddPermission(BloggingPermissions.Tags.Default, L("Permission:Tags"));
        tags.AddChild(BloggingPermissions.Tags.Update, L("Permission:Edit"));
        tags.AddChild(BloggingPermissions.Tags.Delete, L("Permission:Delete"));
        tags.AddChild(BloggingPermissions.Tags.Create, L("Permission:Create"));

        var comments = bloggingGroup.AddPermission(BloggingPermissions.Comments.Default, L("Permission:Comments"));
        comments.AddChild(BloggingPermissions.Comments.Update, L("Permission:Edit"));
        comments.AddChild(BloggingPermissions.Comments.Delete, L("Permission:Delete"));
        comments.AddChild(BloggingPermissions.Comments.Create, L("Permission:Create"));
    }

        // 使用本地化字串進行文字顯示。
    private static LocalizableString L(string name)
    {
        return LocalizableString.Create<BloggingResource>(name);
    }
}

從上面的程式碼就可以看出來,許可權被 ABP vNext 分成了 許可權組定義許可權定義,這兩個東西我們後面進行重點講述。那麼這些 Provider 在什麼時候被執行呢?找到許可權模組的定義,可以看到如下程式碼:

[DependsOn(
    typeof(AbpSecurityModule),
    typeof(AbpLocalizationAbstractionsModule),
    typeof(AbpMultiTenancyModule)
    )]
public class AbpAuthorizationModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        // 在 AutoFac 進行元件註冊的時候,根據元件的型別定義視情況繫結攔截器。
        context.Services.OnRegistred(AuthorizationInterceptorRegistrar.RegisterIfNeeded);

        // 在 AutoFac 進行元件註冊的時候,根據元件的型別,判斷是否是 Provider。
        AutoAddDefinitionProviders(context.Services);
    }

    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // 註冊認證授權服務。
        context.Services.AddAuthorization();

        // 替換掉 ASP.NET Core 提供的許可權處理器,轉而使用 ABP vNext 提供的許可權處理器。
        context.Services.AddSingleton<IAuthorizationHandler, PermissionRequirementHandler>();

        // 這一部分是新增內建的一些許可權值檢查,後面我們在將 PermissionChecker 的時候會提到。
        Configure<PermissionOptions>(options =>
        {
            options.ValueProviders.Add<UserPermissionValueProvider>();
            options.ValueProviders.Add<RolePermissionValueProvider>();
            options.ValueProviders.Add<ClientPermissionValueProvider>();
        });
    }

    private static void AutoAddDefinitionProviders(IServiceCollection services)
    {
        var definitionProviders = new List<Type>();

        services.OnRegistred(context =>
        {
            if (typeof(IPermissionDefinitionProvider).IsAssignableFrom(context.ImplementationType))
            {
                definitionProviders.Add(context.ImplementationType);
            }
        });

        // 將獲取到的 Provider 傳遞給 PermissionOptions 。
        services.Configure<PermissionOptions>(options =>
        {
            options.DefinitionProviders.AddIfNotContains(definitionProviders);
        });
    }
}

可以看到在註冊元件的時候,ABP vNext 就會將這些 Provider 傳遞給 PermissionOptions ,我們根據 DefinitionProviders 欄位找到有一個地方會使用到它,就是 PermissionDefinitionManager 型別的 CreatePermissionGroupDefinitions() 方法。

protected virtual Dictionary<string, PermissionGroupDefinition> CreatePermissionGroupDefinitions()
{
    //  建立一個許可權定義上下文。
    var context = new PermissionDefinitionContext();

    // 建立一個臨時範圍用於解析 Provider,Provider 解析完成之後即被釋放。
    using (var scope = _serviceProvider.CreateScope())
    {
        // 根據之前的型別,通過 IoC 進行解析出例項,指定各個 Provider 的 Define() 方法,會向許可權上下文填充許可權。
        var providers = Options
            .DefinitionProviders
            .Select(p => scope.ServiceProvider.GetRequiredService(p) as IPermissionDefinitionProvider)
            .ToList();

        foreach (var provider in providers)
        {
            provider.Define(context);
        }
    }

    // 返回許可權組名稱 - 許可權組定義的字典。
    return context.Groups;
}

你可能會奇怪,為什麼返回的是一個許可權組名字和定義的鍵值對,而不是返回的許可權資料,我們之前新增的許可權去哪兒了呢?

2.2.2 許可權和許可權組的定義

要搞清楚這個問題,我們首先要知道許可權與許可權組之間的關係是怎樣的。回想我們之前在 Provider 裡面新增許可權的程式碼,首先我們是構建了一個許可權組,然後往許可權組裡面新增的許可權。許可權組的作用就是將許可權按照組的形式進行劃分,方便程式碼進行訪問於管理。

public class PermissionGroupDefinition
{
    /// <summary>
    /// 唯一的許可權組標識名稱。
    /// </summary>
    public string Name { get; }

    // 開發人員針對許可權組的一些自定義屬性。
    public Dictionary<string, object> Properties { get; }

    // 許可權所對應的本地化名稱。
    public ILocalizableString DisplayName
    {
        get => _displayName;
        set => _displayName = Check.NotNull(value, nameof(value));
    }
    private ILocalizableString _displayName;

    /// <summary>
    /// 許可權的適用範圍,預設是租戶/租主都適用。
    /// 預設值: <see cref="MultiTenancySides.Both"/>
    /// </summary>
    public MultiTenancySides MultiTenancySide { get; set; }

    // 許可權組下面的所屬許可權。
    public IReadOnlyList<PermissionDefinition> Permissions => _permissions.ToImmutableList();
    private readonly List<PermissionDefinition> _permissions;

    // 針對於自定義屬性的快捷索引器。
    public object this[string name]
    {
        get => Properties.GetOrDefault(name);
        set => Properties[name] = value;
    }

    protected internal PermissionGroupDefinition(
        string name, 
        ILocalizableString displayName = null,
        MultiTenancySides multiTenancySide = MultiTenancySides.Both)
    {
        Name = name;
        // 沒有傳遞多語言串,則使用許可權組的唯一標識作為顯示內容。
        DisplayName = displayName ?? new FixedLocalizableString(Name);
        MultiTenancySide = multiTenancySide;

        Properties = new Dictionary<string, object>();
        _permissions = new List<PermissionDefinition>();
    }

    // 像許可權組新增屬於它的許可權。
    public virtual PermissionDefinition AddPermission(
        string name, 
        ILocalizableString displayName = null,
        MultiTenancySides multiTenancySide = MultiTenancySides.Both)
    {
        var permission = new PermissionDefinition(name, displayName, multiTenancySide);

        _permissions.Add(permission);

        return permission;
    }

    // 遞迴構建許可權集合,因為定義的某個許可權內部還擁有子許可權。
    public virtual List<PermissionDefinition> GetPermissionsWithChildren()
    {
        var permissions = new List<PermissionDefinition>();

        foreach (var permission in _permissions)
        {
            AddPermissionToListRecursively(permissions, permission);
        }

        return permissions;
    }

    // 遞迴構建方法。
    private void AddPermissionToListRecursively(List<PermissionDefinition> permissions, PermissionDefinition permission)
    {
        permissions.Add(permission);

        foreach (var child in permission.Children)
        {
            AddPermissionToListRecursively(permissions, child);
        }
    }

    public override string ToString()
    {
        return $"[{nameof(PermissionGroupDefinition)} {Name}]";
    }
}

通過許可權組的定義程式碼你就會知道,現在我們的所有許可權都會歸屬於某個許可權組,這一點從之前 Provider 的 IPermissionDefinitionContext 就可以看出來。在許可權上下文內部只允許我們通過 AddGroup() 來新增一個許可權組,之後再通過許可權組的 AddPermission() 方法新增它裡面的許可權。

許可權的定義類叫做 PermissionDefinition,這個型別的構造與許可權組定義類似,沒有什麼好說的。

public class PermissionDefinition
{
    /// <summary>
    /// 唯一的許可權標識名稱。
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// 當前許可權的父級許可權,這個屬性的值只可以通過 AddChild() 方法進行設定。
    /// </summary>
    public PermissionDefinition Parent { get; private set; }

    /// <summary>
    /// 許可權的適用範圍,預設是租戶/租主都適用。
    /// 預設值: <see cref="MultiTenancySides.Both"/>
    /// </summary>
    public MultiTenancySides MultiTenancySide { get; set; }

    /// <summary>
    /// 適用的許可權值提供者,這塊我們會在後面進行講解,為空的時候則使用所有的提供者進行校驗。
    /// </summary>
    public List<string> Providers { get; } //TODO: Rename to AllowedProviders?

    // 許可權的多語言名稱。
    public ILocalizableString DisplayName
    {
        get => _displayName;
        set => _displayName = Check.NotNull(value, nameof(value));
    }
    private ILocalizableString _displayName;

    // 獲取許可權的子級許可權。
    public IReadOnlyList<PermissionDefinition> Children => _children.ToImmutableList();
    private readonly List<PermissionDefinition> _children;

    /// <summary>
    /// 開發人員針對許可權的一些自定義屬性。
    /// </summary>
    public Dictionary<string, object> Properties { get; }

    // 針對於自定義屬性的快捷索引器。
    public object this[string name]
    {
        get => Properties.GetOrDefault(name);
        set => Properties[name] = value;
    }

    protected internal PermissionDefinition(
        [NotNull] string name, 
        ILocalizableString displayName = null,
        MultiTenancySides multiTenancySide = MultiTenancySides.Both)
    {
        Name = Check.NotNull(name, nameof(name));
        DisplayName = displayName ?? new FixedLocalizableString(name);
        MultiTenancySide = multiTenancySide;

        Properties = new Dictionary<string, object>();
        Providers = new List<string>();
        _children = new List<PermissionDefinition>();
    }

    public virtual PermissionDefinition AddChild(
        [NotNull] string name, 
        ILocalizableString displayName = null,
        MultiTenancySides multiTenancySide = MultiTenancySides.Both)
    {
        var child = new PermissionDefinition(
            name, 
            displayName, 
            multiTenancySide)
        {
            Parent = this
        };

        _children.Add(child);

        return child;
    }

    /// <summary>
    /// 設定指定的自定義屬性。
    /// </summary>
    public virtual PermissionDefinition WithProperty(string key, object value)
    {
        Properties[key] = value;
        return this;
    }

    /// <summary>
    /// 新增一組許可權值提供者集合。
    /// </summary>
    public virtual PermissionDefinition WithProviders(params string[] providers)
    {
        if (!providers.IsNullOrEmpty())
        {
            Providers.AddRange(providers);
        }

        return this;
    }

    public override string ToString()
    {
        return $"[{nameof(PermissionDefinition)} {Name}]";
    }
}

2.2.3 許可權管理器

繼續回到許可權管理器,許可權管理器的介面定義是 IPermissionDefinitionManager ,從介面的方法定義來看,都是獲取許可權的方法,說明許可權管理器主要提供給其他元件進行許可權校驗操作。

public interface IPermissionDefinitionManager
{
    // 根據許可權定義的唯一標識獲取許可權,一旦不存在就會丟擲 AbpException 異常。
    [NotNull]
    PermissionDefinition Get([NotNull] string name);

    // 根據許可權定義的唯一標識獲取許可權,如果許可權不存在,則返回 null。
    [CanBeNull]
    PermissionDefinition GetOrNull([NotNull] string name);

    // 獲取所有的許可權。
    IReadOnlyList<PermissionDefinition> GetPermissions();
    
    // 獲取所有的許可權組。
    IReadOnlyList<PermissionGroupDefinition> GetGroups();
}

接著我們來回答 2.2.1 末尾提出的問題,許可權組是根據 Provider 自動建立了,那麼許可權呢?其實我們在許可權管理器裡面拿到了許可權組,許可權定義就很好構建了,直接遍歷所有許可權組拿它們的 Permissions 屬性構建即可。

protected virtual Dictionary<string, PermissionDefinition> CreatePermissionDefinitions()
{
    var permissions = new Dictionary<string, PermissionDefinition>();

    // 遍歷許可權定義組,這個東西在之前就已經構建好了。
    foreach (var groupDefinition in PermissionGroupDefinitions.Values)
    {
        // 遞迴子級許可權。
        foreach (var permission in groupDefinition.Permissions)
        {
            AddPermissionToDictionaryRecursively(permissions, permission);
        }
    }

    // 返回許可權唯一標識 - 許可權定義 的字典。
    return permissions;
}

protected virtual void AddPermissionToDictionaryRecursively(
    Dictionary<string, PermissionDefinition> permissions, 
    PermissionDefinition permission)
{
    if (permissions.ContainsKey(permission.Name))
    {
        throw new AbpException("Duplicate permission name: " + permission.Name);
    }

    permissions[permission.Name] = permission;

    foreach (var child in permission.Children)
    {
        AddPermissionToDictionaryRecursively(permissions, child);
    }
}

2.2.4 授權策略提供者的實現

我們發現 ABP vNext 自己實現了 IAbpAuthorizationPolicyProvider 介面,實現的型別就是 AbpAuthorizationPolicyProvider

這個型別它是繼承的 DefaultAuthorizationPolicyProvider ,重寫了 GetPolicyAsync() 方法,目的就是將 PermissionDefinition 轉換為 AuthorizationPolicy

如果去看了 雨夜朦朧 大神的部落格,就知道我們一個授權策略可以由多個條件構成。也就是說某一個 AuthorizationPolicy 可以擁有多個限定條件,當所有限定條件被滿足之後,才能算是通過許可權驗證,例如以下程式碼。

public void ConfigureService(IServiceCollection services)
{
    services.AddAuthorization(options =>
    {
        options.AddPolicy("User", policy => policy
            .RequireAssertion(context => context.User.HasClaim(c => (c.Type == "EmployeeNumber" || c.Type == "Role")))
        );

        // 這裡的意思是,使用者角色必須是 Admin,並且他的使用者名稱是 Alice,並且必須要有型別為 EmployeeNumber 的 Claim。
        options.AddPolicy("Employee", policy => policy
            .RequireRole("Admin")
            .RequireUserName("Alice")
            .RequireClaim("EmployeeNumber")
            .Combine(commonPolicy));
    });
}

這裡的 RequireRole()RequireUserName()RequireClaim() 都會生成一個 IAuthorizationRequirement 物件,它們在內部有不同的實現規則。

public AuthorizationPolicyBuilder RequireClaim(string claimType)
{
    if (claimType == null)
    {
        throw new ArgumentNullException(nameof(claimType));
    }

    // 構建了一個 ClaimsAuthorizationRequirement 物件,並新增到策略的 Requirements 組。
    Requirements.Add(new ClaimsAuthorizationRequirement(claimType, allowedValues: null));
    return this;
}

這裡我們 ABP vNext 則是使用的 PermissionRequirement 作為一個限定條件。

public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
    var policy = await base.GetPolicyAsync(policyName);
    if (policy != null)
    {
        return policy;
    }

    var permission = _permissionDefinitionManager.GetOrNull(policyName);
    if (permission != null)
    {
        // TODO: 可以使用快取進行優化。
        // 通過 Builder 構建一個策略。
        var policyBuilder = new AuthorizationPolicyBuilder(Array.Empty<string>());
        // 建立一個 PermissionRequirement 物件新增到限定條件組中。
        policyBuilder.Requirements.Add(new PermissionRequirement(policyName));
        return policyBuilder.Build();
    }

    return null;
}

ClaimsAuthorizationRequirement 不同的是,ABP vNext 並沒有將限定條件處理器和限定條件定義放在一起實現,而是分開的,分別構成了 PermissionRequirementPermissionRequirementHandler ,後者在模組配置的時候被注入到 IoC 裡面。

PS:

對於 Handler 來說,我們可以編寫多個 Handler 注入到 IoC 容器內部,如下程式碼:

services.AddSingleton<IAuthorizationHandler, BadgeEntryHandler>();
services.AddSingleton<IAuthorizationHandler, HasTemporaryStickerHandler>();

首先看限定條件 PermissionRequirement 的定義,非常簡單。

public class PermissionRequirement : IAuthorizationRequirement
{
    public string PermissionName { get; }

    public PermissionRequirement([NotNull]string permissionName)
    {
        Check.NotNull(permissionName, nameof(permissionName));

        PermissionName = permissionName;
    }
}

在限定條件內部,我們只用了許可權的唯一標識來進行處理,接下來看一下許可權處理器。

public class PermissionRequirementHandler : AuthorizationHandler<PermissionRequirement>
{
        // 這裡通過許可權檢查器來確定當前使用者是否擁有某個許可權。
    private readonly IPermissionChecker _permissionChecker;

    public PermissionRequirementHandler(IPermissionChecker permissionChecker)
    {
        _permissionChecker = permissionChecker;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement)
    {
            // 如果當前使用者擁有某個許可權,則通過 Contxt.Succeed() 通過授權驗證。
        if (await _permissionChecker.IsGrantedAsync(context.User, requirement.PermissionName))
        {
            context.Succeed(requirement);
        }
    }
}

2.2.5 許可權檢查器

在上面的處理器我們看到了,ABP vNext 是通過許可權檢查器來校驗某個使用者是否滿足某個授權策略,先看一下 IPermissionChecker 介面的定義,基本都是傳入身份證(ClaimsPrincipal)和需要校驗的許可權進行處理。

public interface IPermissionChecker
{
    Task<bool> IsGrantedAsync([NotNull]string name);

    Task<bool> IsGrantedAsync([CanBeNull] ClaimsPrincipal claimsPrincipal, [NotNull]string name);
}

第一個方法內部就是呼叫的第二個方法,只不過傳遞的身份證是通過 ICurrentPrincipalAccessor 拿到的,所以我們的核心還是看第二個方法的實現。

public virtual async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
{
    Check.NotNull(name, nameof(name));

    var permission = PermissionDefinitionManager.Get(name);

    var multiTenancySide = claimsPrincipal?.GetMultiTenancySide()
                            ?? CurrentTenant.GetMultiTenancySide();

    // 檢查傳入的許可權是否允許當前的使用者模式(租戶/租主)進行訪問。
    if (!permission.MultiTenancySide.HasFlag(multiTenancySide))
    {
        return false;
    }

    var isGranted = false;
    // 這裡是重點哦,這個許可權值檢測上下文是之前沒有說過的東西,說白了就是針對不同維度的許可權檢測。
    // 之前這部分東西是通過許可權策略下面的 Requirement 提供的,這裡 ABP vNext 將其抽象為 PermissionValueProvider。
    var context = new PermissionValueCheckContext(permission, claimsPrincipal);
    foreach (var provider in PermissionValueProviderManager.ValueProviders)
    {
        // 如果指定的許可權允許的許可權值提供者集合不包含當前的 Provider,則跳過處理。
        if (context.Permission.Providers.Any() &&
            !context.Permission.Providers.Contains(provider.Name))
        {
            continue;
        }

        // 呼叫 Provider 的檢測方法,傳入身份證明和許可權定義進行具體校驗。
        var result = await provider.CheckAsync(context);

        // 根據返回的結果,判斷是否通過了許可權校驗。
        if (result == PermissionGrantResult.Granted)
        {
            isGranted = true;
        }
        else if (result == PermissionGrantResult.Prohibited)
        {
            return false;
        }
    }

    // 返回 true 說明已經授權,返回 false 說明是沒有授權的。
    return isGranted;
}

2.2.6 PermissionValueProvider

在模組配置方法內部,可以看到通過 Configure<PermissionOptions>() 方法新增了三個 PermissionValueProvider ,即 UserPermissionValueProviderRolePermissionValueProviderClientPermissionValueProvider 。在它們的內部實現,都是通過 IPermissionStore 從持久化儲存 檢查傳入的使用者是否擁有某個許可權

這裡我們以 UserPermissionValueProvider 為例,來看看它的實現方法。

public class UserPermissionValueProvider : PermissionValueProvider
{
    // 提供者的名稱。
    public const string ProviderName = "User";

    public override string Name => ProviderName;

    public UserPermissionValueProvider(IPermissionStore permissionStore)
        : base(permissionStore)
    {

    }

    public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
    {
        // 從傳入的 Principal 中查詢 UserId,不存在則說明沒有定義,視為未授權。
        var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;

        if (userId == null)
        {
            return PermissionGrantResult.Undefined;
        }

        // 呼叫 IPermissionStore 從持久化儲存中,檢測指定許可權在某個提供者下面是否已經被授予了許可權。
        // 如果被授予了許可權, 則返回 true,沒有則返回 false。
        return await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, userId)
            ? PermissionGrantResult.Granted
            : PermissionGrantResult.Undefined;
    }
}

這裡我們先不講 IPermissionStore 的具體實現,就上述程式碼來看,ABP vNext 是將許可權定義放在了一個管理容器(IPermissionDeftiionManager)。然後又實現了自定義的策略處理器和策略,在處理器的內部又通過 IPermissionChecker 根據不同的 PermissionValueProvider 結合 IPermissionStore 實現了指定使用者標識到許可權的檢測功能。

2.2.7 許可權驗證攔截器

許可權驗證攔截器的註冊都是在 AuthorizationInterceptorRegistrarRegisterIfNeeded() 方法內實現的,只要型別的任何一個方法標註了 AuthorizeAttribute 特性,就會被關聯攔截器。

private static bool AnyMethodHasAuthorizeAttribute(Type implementationType)
{
    return implementationType
        .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
        .Any(HasAuthorizeAttribute);
}

private static bool HasAuthorizeAttribute(MemberInfo methodInfo)
{
    return methodInfo.IsDefined(typeof(AuthorizeAttribute), true);
}

攔截器和型別關聯之後,會通過 IMethodInvocationAuthorizationServiceCheckAsync() 方法校驗呼叫者是否擁有指定許可權。

public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
    // 防止重複檢測。
    if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Authorization))
    {
        await invocation.ProceedAsync();
        return;
    }

    // 將被呼叫的方法傳入,驗證是否允許訪問。
    await AuthorizeAsync(invocation);
    await invocation.ProceedAsync();
}

protected virtual async Task AuthorizeAsync(IAbpMethodInvocation invocation)
{
    await _methodInvocationAuthorizationService.CheckAsync(
        new MethodInvocationAuthorizationContext(
            invocation.Method
        )
    );
}

在具體的實現當中,首先檢測方法是否標註了 IAllowAnonymous 特性,標註了則說明允許匿名訪問,直接返回不做任何處理。否則就會從方法獲取實現了 IAuthorizeData 介面的特性,從裡面拿到 Policy 值,並通過 IAuthorizationService 進行驗證。

protected async Task CheckAsync(IAuthorizeData authorizationAttribute)
{
    if (authorizationAttribute.Policy == null)
    {
        // 如果當前呼叫者沒有進行認證,則丟擲未登入的異常。
        if (!_currentUser.IsAuthenticated && !_currentClient.IsAuthenticated)
        {
            throw new AbpAuthorizationException("Authorization failed! User has not logged in.");
        }
    }
    else
    {
        // 通過 IAuthorizationService 校驗當前使用者是否擁有 authorizationAttribute.Policy 許可權。
        await _authorizationService.CheckAsync(authorizationAttribute.Policy);
    }
}

針對於 IAuthorizationService ,ABP vNext 還是提供了自己的實現 AbpAuthorizationService,裡面沒有重寫什麼方法,而是提供了兩個新的屬性,這兩個屬性是為了方便實現 AbpAuthorizationServiceExtensions 提供的擴充套件方法,這裡不再贅述。

三、總結

關於許可權與驗證部分我就先講到這兒,後續文章我會更加詳細地為大家分析 ABP vNext 是如何進行許可權管理,又是如何將 ABP vNext 和 ASP.NET Identity 、IdentityServer4 進行整合的。

相關文章