1. 程式人生 > >ASP.NET Core策略授權和 ABP 授權

ASP.NET Core策略授權和 ABP 授權

[TOC] Github 倉庫原始碼地址 [https://github.com/whuanles/2020-07-12](https://github.com/whuanles/2020-07-12) ## ASP.NET Core 中的策略授權 首先我們來建立一個 WebAPI 應用。 然後引入 `Microsoft.AspNetCore.Authentication.JwtBearer` 包。 ### 策略 Startup 類的 ConfigureServices 方法中,新增一個策略的形式如下: ```csharp services.AddAuthorization(options => { options.AddPolicy("AtLeast21", policy => policy.Requirements.Add(new MinimumAgeRequirement(21))); }); ``` 這裡我們分步來說。 services.AddAuthorization 用於新增授權方式,目前只支援 AddPolicy。 ASP.NET Core 中,有基於角色、宣告、策略的三種授權形式,都是使用 `AddPolicy` 來新增授權處理。 其中,有兩個 API 如下: ```csharp public void AddPolicy(string name, AuthorizationPolicy policy); public void AddPolicy(string name, Action configurePolicy); ``` `name = "AtLeast21"`,這裡 "AtLeast21" 是策略的名稱。 `policy.Requirements.Add()` 用於新增一個策略的標記(儲存此策略的資料),此標記需要繼承 `IAuthorizationRequirement` 介面。 策略的名稱應該如何設定呢?在授權上應該如何編寫策略以及使用 `Requirements.Add()`? 這裡先放一放,我們接下來再講解。 ### 定義一個 Controller 我們來新增一個 Controller : ```csharp [ApiController] [Route("[controller]")] public class BookController : ControllerBase { private static List BookContent = new List(); [HttpGet("Add")] public string AddContent(string body) { BookContent.Add(body); return "success"; } [HttpGet("Remove")] public string RemoveContent(int n) { BookContent.Remove(BookContent[n]); return "success"; } [HttpGet("Select")] public List SelectContent() { List obj = new List(); int i = 0; foreach (var item in BookContent) { int tmp = i; i++; obj.Add(new { Num = tmp, Body = item }); } return obj; } [HttpGet("Update")] public string UpdateContent(int n, string body) { BookContent[n] = body; return "success"; } } ``` 功能很簡單,就是對列表內容增刪查改。 ### 設定許可權 前面我們建立了 `BookController` ,具有增刪查改的功能。應該為每一個功能都應該設定一種許可權。 ASP.NET Core 中,一個許可權標記,需要繼承`IAuthorizationRequirement` 介面。 我們來設定五個許可權: 新增一個檔案,填寫以下程式碼。 ```csharp /* IAuthorizationRequirement 是一個空介面,具體對於授權的需求,其屬性等資訊是自定義的 這裡的繼承關係也沒有任何意義 */ // 能夠訪問 Book 的許可權 public class BookRequirment : IAuthorizationRequirement { } // 增刪查改 Book 許可權 // 可以繼承 IAuthorizationRequirement ,也可以繼承 BookRequirment public class BookAddRequirment : BookRequirment { } public class BookRemoveRequirment : BookRequirment { } public class BookSelectRequirment : BookRequirment { } public class BookUpdateRequirment : BookRequirment { } ``` BookRequirment 代表能夠訪問 BookController,其它四個分別代表增刪查改的許可權。 ### 定義策略 許可權設定後,我們開始設定策略。 在 Startup 的 `ConfigureServices` 中,新增: ```csharp services.AddAuthorization(options => { options.AddPolicy("Book", policy => { policy.Requirements.Add(new BookRequirment()); }); options.AddPolicy("Book:Add", policy => { policy.Requirements.Add(new BookAddRequirment()); }); options.AddPolicy("Book:Remove", policy => { policy.Requirements.Add(new BookRemoveRequirment()); }); options.AddPolicy("Book:Select", policy => { policy.Requirements.Add(new BookSelectRequirment()); }); options.AddPolicy("Book:Update", policy => { policy.Requirements.Add(new BookUpdateRequirment()); }); }); ``` 這裡我們為每種策略只設置一種許可權,當然每種策略都可以新增多個許可權, 這裡名稱使用 `:` 隔開,主要是為了可讀性,讓人一看就知道是層次關係。 ### 儲存使用者資訊 這裡為了更加簡單,就不使用資料庫了。 以下使用者資訊結構是隨便寫的。使用者-角色-角色具有的許可權。 這個許可權用什麼型別儲存都可以。只要能夠標識區分是哪個許可權就行。 ```csharp /// /// 儲存使用者資訊 ///
public static class UsersData { public static readonly List Users = new List(); static UsersData() { // 新增一個管理員 Users.Add(new User { Name = "admin", Email = "[email protected]", Role = new Role { Requirements = new List { typeof( BookRequirment), typeof( BookAddRequirment), typeof( BookRemoveRequirment), typeof( BookSelectRequirment), typeof( BookUpdateRequirment) } } }); // 沒有刪除許可權 Users.Add(new User { Name = "作者", Email = "wirter", Role = new Role { Requirements = new List { typeof( BookRequirment), typeof( BookAddRequirment), typeof( BookRemoveRequirment), typeof( BookSelectRequirment), } } }); } } public class User { public string Name { get; set; } public string Email { get; set; } public Role Role { get; set; } } /// /// 這裡的儲存角色的策略授權,字串數字等都行,只要能夠儲存表示就OK /// 在這裡沒有任何意義,只是標識的一種方式
///
public class Role { public List Requirements { get; set; } } ``` ### 標記訪問許可權 定義策略完畢後,就要為 Controller 和 Action 標記訪問許可權了。 使用 `[Authorize(Policy = "{string}")]` 特性和屬性來設定訪問此 Controller 、 Action 所需要的許可權。 這裡我們分開設定,每個功能標記一種許可權(最小粒度應該是一個功能 ,而不是一個 API)。 ```csharp [Authorize(Policy = "Book")] [ApiController] [Route("[controller]")] public class BookController : ControllerBase { private static List BookContent = new List(); [Authorize(Policy = "Book:Add")] [HttpGet("Add")] public string AddContent(string body){} [Authorize(Policy = "Book:Remove")] [HttpGet("Remove")] public string RemoveContent(int n){} [Authorize(Policy = "Book:Select")] [HttpGet("Select")] public List SelectContent(){} [Authorize(Policy = "Book:Update")] [HttpGet("Update")] public string UpdateContent(int n, string body){} } ``` ### 認證:Token 憑據 因為使用的是 WebAPI,所以使用 Bearer Token 認證,當然使用 Cookie 等也可以。使用什麼認證方式都可以。 ```csharp // 設定驗證方式為 Bearer Token // 新增 using Microsoft.AspNetCore.Authentication.JwtBearer; // 你也可以使用 字串 "Brearer" 代替 JwtBearerDefaults.AuthenticationScheme services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234")), // 加密解密Token的金鑰 // 是否驗證釋出者 ValidateIssuer = true, // 釋出者名稱 ValidIssuer = "server", // 是否驗證訂閱者 // 訂閱者名稱 ValidateAudience = true, ValidAudience = "client007", // 是否驗證令牌有效期 ValidateLifetime = true, // 每次頒發令牌,令牌有效時間 ClockSkew = TimeSpan.FromMinutes(120) }; }); ``` 上面的程式碼是一個模板,可以隨便改。這裡的認證方式跟我們的策略授權沒什麼關係。 ### 頒發登入憑據 下面這個 Action 放置到 BookController,作為登入功能。這一部分也不重要,主要是為使用者頒發憑據,以及標識使用者。使用者的 Claim 可以儲存此使用者的唯一標識。 ```csharp /// /// 使用者登入並且頒發憑據 ///
/// /// [AllowAnonymous] [HttpGet("Token")] public string Token(string name) { User user = UsersData.Users.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (user is null) return "未找到此使用者"; // 定義使用者資訊 var claims = new Claim[] { new Claim(ClaimTypes.Name, name), new Claim(JwtRegisteredClaimNames.Email, user.Email) }; // 和 Startup 中的配置一致 SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234")); JwtSecurityToken token = new JwtSecurityToken( issuer: "server", audience: "client007", claims: claims, notBefore: DateTime.Now, expires: DateTime.Now.AddMinutes(30), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) ); string jwtToken = new JwtSecurityTokenHandler().WriteToken(token); return jwtToken; } ``` Configure 中補充以下兩行: ```csharp app.UseAuthentication(); app.UseAuthorization(); ``` ### 自定義授權 自定義授權需要繼承 `IAuthorizationHandler` 介面,實現此介面的類能夠決定是否對使用者的訪問進行授權。 實現程式碼如下: ```csharp /// /// 判斷使用者是否具有許可權 /// public class PermissionHandler : IAuthorizationHandler { public async Task HandleAsync(AuthorizationHandlerContext context) { // 當前訪問 Controller/Action 所需要的許可權(策略授權) IAuthorizationRequirement[] pendingRequirements = context.PendingRequirements.ToArray(); // 取出使用者資訊 IEnumerable claims = context.User?.Claims; // 未登入或者取不到使用者資訊 if (claims is null) { context.Fail(); return; } // 取出使用者名稱 Claim userName = claims.FirstOrDefault(x => x.Type == ClaimTypes.Name); if (userName is null) { context.Fail(); return; } // ... 省略一些檢驗過程 ... // 獲取此使用者的資訊 User user = UsersData.Users.FirstOrDefault(x => x.Name.Equals(userName.Value, StringComparison.OrdinalIgnoreCase)); List auths = user.Role.Requirements; // 逐個檢查 foreach (IAuthorizationRequirement requirement in pendingRequirements) { // 如果使用者許可權列表中沒有找到此許可權的話 if (!auths.Any(x => x == requirement.GetType())) context.Fail(); context.Succeed(requirement); } await Task.CompletedTask; } } ``` 過程: - 從上下文(Context) 中獲取使用者資訊(context.User) - 獲取此使用者所屬的角色,並獲取此角色具有的許可權 - 獲取此次請求的 Controller/Action 需要的許可權(context.PendingRequirements) - 檢查所需要的許可權(foreach迴圈),此使用者是否都具有 最後需要將此介面、服務,註冊到容器中: ```csharp services.AddSingleton(); ``` 做完這些後,就可以測試授權了。 ### IAuthorizationService 前面實現了 IAuthorizationHandler 介面的類,用於自定義確定使用者是否有權訪問此 Controller/Action。 IAuthorizationService 介面用於確定授權是否成功,其定義如下: ```csharp public interface IAuthorizationService { Task AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable requirements); Task AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName); } ``` `DefaultAuthorizationService ` 介面實現了 `IAuthorizationService` ,ASP.NET Core 預設使用 `DefaultAuthorizationService ` 來確認授權。 前面我們使用 `IAuthorizationHandler` 介面來自定義授權,如果再深入一層的話,就追溯到了`IAuthorizationService`。 `DefaultAuthorizationService ` 是 `IAuthorizationService` 的預設實現,其中有一段程式碼如下: ![](https://img2020.cnblogs.com/blog/1315495/202007/1315495-20200712211819835-2104456854.png) `DefaultAuthorizationService ` 比較複雜,一般情況下,我們只要實現 `IAuthorizationHandler` ` 就夠了。 參考資料:[https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.authorization.defaultauthorizationservice?view=aspnetcore-3.1](https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.authorization.defaultauthorizationservice?view=aspnetcore-3.1) ## ABP 授權 前面已經介紹了 ASP.NET Core 中的策略授權,這裡介紹一下 ABP 中的授權,我們繼續利用前面已經實現的 ASP.NET Core 程式碼。 ### 建立 ABP 應用 Nuget 安裝 `Volo.Abp.AspNetCore.Mvc`、`Volo.Abp.Autofac` 。 建立 `AppModule` 類,程式碼如下: ```csharp [DependsOn(typeof(AbpAspNetCoreMvcModule))] [DependsOn(typeof(AbpAutofacModule))] public class AppModule : AbpModule { public override void OnApplicationInitialization( ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseConfiguredEndpoints(); } } ``` 在 Program 的 Host 加上 `.UseServiceProviderFactory(new AutofacServiceProviderFactory())`,示例如下: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseServiceProviderFactory(new AutofacServiceProviderFactory()) ... ... ``` 然後在 Startup 中的 `ConfiguraServices` 方法中,新增 ABP 模組, 並且設定使用 Autofac。 ```csharp public void ConfigureServices(IServiceCollection services) { services.AddApplication(options=> { options.UseAutofac(); }); } ``` ### 定義許可權 ABP 中使用 `PermissionDefinitionProvider` 類來定義許可權,建立一個類,其程式碼如下: ```csharp public class BookPermissionDefinitionProvider : PermissionDefinitionProvider { public override void Define(IPermissionDefinitionContext context) { var myGroup = context.AddGroup("Book"); var permission = myGroup.AddPermission("Book"); permission.AddChild("Book:Add"); permission.AddChild("Book:Remove"); permission.AddChild("Book:Select"); permission.AddChild("Book:Update"); } } ``` 這裡定義了一個組 `Book`,定義了一個許可權 `Book`了,`Book` 其下有四個子許可權。 刪除 Startup 中的`services.AddAuthorization(options =>...` 。 將剩餘的依賴注入服務程式碼移動到 AppModule 的 `ConfigureServices` 中。 Startup 的 Configure 改成: ```csharp app.InitializeApplication(); ``` AbpModule 中的 `Configure` 改成: ```csharp var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseConfiguredEndpoints(); ``` PermissionHandler 需要改成: ```csharp public class PermissionHandler : IAuthorizationHandler { public Task HandleAsync(AuthorizationHandlerContext context) { // 當前訪問 Controller/Action 所需要的許可權(策略授權) IAuthorizationRequirement[] pendingRequirements = context.PendingRequirements.ToArray(); // 逐個檢查 foreach (IAuthorizationRequirement requirement in pendingRequirements) { context.Succeed(requirement); } return Task.CompletedTask; } } ``` 刪除 UserData 檔案;BookController 需要修改一下登入和憑證。 具體細節可參考倉庫