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


一、簡要說明

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

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

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

二、源碼分析

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

2.1 Security 基礎組件庫

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

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 進行集成的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM