在上篇文章裏面,咱們在 ApplicationService
當中看到了權限檢測代碼,經過注入 IAuthorizationService
就能夠實現權限檢測。不過跳轉到源碼才發現,這個接口是 ASP.NET Core 原生提供的 「基於策略」 的權限驗證接口,這就說明 ABP vNext 基於原生的受權驗證框架進行了自定義擴展。html
讓咱們來看一下 Volo.Abp.Ddd.Application 項目的依賴結構(權限相關)。緩存
本篇文章下面的內容基本就會圍繞上述框架模塊展開,本篇文章通篇較長,由於還涉及到 .NET Core Identity 與 IdentityServer4 這兩部分。關於這兩部分的內容,我會在本篇文章大概講述 ABP vNext 的實現,關於更加詳細的內容,請查閱官方文檔或其餘博主的博客。框架
ABP vNext 關於權限驗證和權限定義的部分,都存放在 Volo.Abp.Authorization 和 Volo.Abp.Security 模塊內部。源碼分析我都比較喜歡倒推,即經過實際的使用場景,反向推導 基礎實現,因此後面文章編寫的順序也將會以這種方式進行。異步
這裏咱們先來到 Volo.Abp.Security,由於這個模塊代碼和類型都是最少的。這個項目都沒有模塊定義,說明裏面的東西都是定義的一些基礎組件。async
先從第一個擴展方法開始,這個擴展方法裏面比較簡單,它主要是提供對 ClaimsPrincipal
和 IIdentity
的快捷訪問方法。好比我要從 ClaimsPrincipal
/ IIdentity
獲取租戶 Id、用戶 Id 等。ide
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); }
這個異常咱們在老版本 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 章,我也編寫了相應的 讀書筆記 。學習
開發人員常常會在各類地方須要獲取當前的用戶信息,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()
擴展方法幫助咱們臨時更改當前用戶/租戶。
關於 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 了,這就會產生不可預料的後果。
若是你還想了解更多信息,能夠參考如下兩篇博文:
這一套東西就比較簡單了,是 ABP vNext 爲咱們提供的一套開箱即用組件。開發人員可使用 IStringEncryptionService
來加密/解密你的字符串,默認實現是基於 Rfc2898DeriveBytes
的。關於詳細信息,你能夠閱讀具體的代碼,這裏再也不贅述。
在 Volo.Abp.Authorization 模塊裏面就對權限進行了具體定義,而且基於 ASP.NET Core Authentication 進行無縫集成。若是讀者對於 ASP.NET Core 認證和受權不太瞭解,能夠去學習一下 雨夜朦朧 大神的《ASP.NET Core 認證於受權》系列文章,這裏就再也不贅述。
在 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; }
你可能會奇怪,爲何返回的是一個權限組名字和定義的鍵值對,而不是返回的權限數據,咱們以前添加的權限去哪兒了呢?
要搞清楚這個問題,咱們首先要知道權限與權限組之間的關係是怎樣的。回想咱們以前在 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}]"; } }
繼續回到權限管理器,權限管理器的接口定義是 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); } }
咱們發現 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); } } }
在上面的處理器咱們看到了,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; }
在模塊配置方法內部,能夠看到經過 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
實現了指定用戶標識到權限的檢測功能。
權限驗證攔截器的註冊都是在 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 進行集成的。