天天看點

[Abp vNext 源碼分析] - 7. 權限與驗證

一、簡要說明

在上篇文章裡面,我們在

ApplicationService

當中看到了權限檢測代碼,通過注入

IAuthorizationService

就可以實作權限檢測。不過跳轉到源碼才發現,這個接口是 ASP.NET Core 原生提供的 “基于政策” 的權限驗證接口,這就說明 ABP vNext 基于原生的授權驗證架構進行了自定義擴充。

讓我們來看一下 Volo.Abp.Ddd.Application 項目的依賴結構(權限相關)。

[Abp vNext 源碼分析] - 7. 權限與驗證

本篇文章下面的内容基本就會圍繞上述架構子產品展開,本篇文章通篇較長,因為還涉及到 .NET Core Identity 與 IdentityServer4 這兩部分。關于這兩部分的内容,我會在本篇文章大概講述 ABP vNext 的實作,關于更加詳細的内容,請查閱官方文檔或其他部落客的部落格。

二、源碼分析

ABP vNext 關于權限驗證和權限定義的部分,都存放在 Volo.Abp.Authorization 和 Volo.Abp.Security 子產品内部。源碼分析我都比較喜歡倒推,即通過實際的使用場景,反向推導 基礎實作,是以後面文章編寫的順序也将會以這種方式進行。

2.1 Security 基礎元件庫

這裡我們先來到 Volo.Abp.Security,因為這個子產品代碼和類型都是最少的。這個項目都沒有子產品定義,說明裡面的東西都是定義的一些基礎元件。

[Abp vNext 源碼分析] - 7. 權限與驗證

2.1.1 Claims 與 Identity 的快捷通路

先從第一個擴充方法開始,這個擴充方法裡面比較簡單,它主要是提供對

ClaimsPrincipal

IIdentity

的快捷通路方法。比如我要從

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:

除了

ICurrentUser

ICurrentClient

之外,在 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.CurrentPrincipal

ClaimsPrincipal.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 了,這就會産生不可預料的後果。

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

  • DAVID PINE - 《WHAT HAPPENED TO MY THREAD.CURRENTPRINCIPAL》
  • SCOTT HANSELMAN - 《System.Threading.Thread.CurrentPrincipal vs. System.Web.HttpContext.Current.User or why FormsAuthentication can be subtle》

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 并沒有将限定條件處理器和限定條件定義放在一起實作,而是分開的,分别構成了

PermissionRequirement

PermissionRequirementHandler

,後者在子產品配置的時候被注入到 IoC 裡面。

對于 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

,即

UserPermissionValueProvider

RolePermissionValueProvider

ClientPermissionValueProvider

。在它們的内部實作,都是通過

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 權限驗證攔截器

權限驗證攔截器的注冊都是在

AuthorizationInterceptorRegistrar

RegisterIfNeeded()

方法内實作的,隻要類型的任何一個方法标注了

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);
}
           

攔截器和類型關聯之後,會通過

IMethodInvocationAuthorizationService

CheckAsync()

方法校驗調用者是否擁有指定權限。

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 進行內建的。

  • ABP vNext 系列文章總目錄。
  • 本篇文章的 PDF 版本。