[Abp vNext 源碼分析] – 7. 許可權與驗證
- 2019 年 10 月 3 日
- 筆記
一、簡要說明
在上篇文章裡面,我們在 ApplicationService
當中看到了許可權檢測程式碼,通過注入 IAuthorizationService
就可以實現許可權檢測。不過跳轉到源碼才發現,這個介面是 ASP.NET Core 原生提供的 「基於策略」 的許可權驗證介面,這就說明 ABP vNext 基於原生的授權驗證框架進行了自定義擴展。
讓我們來看一下 Volo.Abp.Ddd.Application 項目的依賴結構(許可權相關)。
本篇文章下面的內容基本就會圍繞上述框架模組展開,本篇文章通篇較長,因為還涉及到 .NET Core Identity 與 IdentityServer4 這兩部分。關於這兩部分的內容,我會在本篇文章大概講述 ABP vNext 的實現,關於更加詳細的內容,請查閱官方文檔或其他部落客的部落格。
二、源碼分析
ABP vNext 關於許可權驗證和許可權定義的部分,都存放在 Volo.Abp.Authorization 和 Volo.Abp.Security 模組內部。源碼分析我都比較喜歡倒推,即通過實際的使用場景,反向推導 基礎實現,所以後面文章編寫的順序也將會以這種方式進行。
2.1 Security 基礎組件庫
這裡我們先來到 Volo.Abp.Security,因為這個模組程式碼和類型都是最少的。這個項目都沒有模組定義,說明裡面的東西都是定義的一些基礎組件。
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 裡面。
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
,即 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 進行集成的。