ASP.NET Core 2.2 : 二十七. JWT與用戶授權(細化到Action)
- 2019 年 10 月 3 日
- 筆記
上一章分享了如何在ASP.NET Core中應用JWT進行用戶認證以及Token的刷新,本章繼續進行下一步,用戶授權。涉及到的例子也以上一章的為基礎。(ASP.NET Core 系列目錄)
一、概述
首先說一下認證(authentication)與授權(authorization),它們經常在一起工作,所以有時候會分不清楚。並且這兩個英文單詞長得也像兄弟。舉例來說,我刷門禁卡進入公司,門禁【認證】了我是這裡的員工,可以進入;但進入公司以後,我並不是所有房間都可以進,比如“機房重地,閑人免進”,我能進入哪些房間,需要公司的【授權】。這就是認證和授權的區別。
ASP.NET Core提倡的是基於聲明(Claim)的授權,關於這個Claim,上一章用到過,有如下這樣的代碼,但沒有介紹:
Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) };
這是一個聲明的集合,它包含了兩個 聲明,用於保存了用戶的唯一ID和用戶名。當然我們還可以添加更多的Claim。對應Claim,還有ClaimsIdentity 和ClaimsPrincipal 兩個類型。
ClaimsIdentity相當於是一個證件,例如上例的門禁卡;ClaimsPrincipal 則是證件的持有者,也就是我本人;那麼對應的Claim就是門禁卡內存儲的一些信息,例如證件號、持有人姓名等。
我除了門禁卡還有身份證、銀行卡等,也就是說一個ClaimsPrincipal中可以有多個ClaimsIdentity,而一個ClaimsIdentity中可以有多個Claim。ASP.NET Core的授權模型大概就是這樣的一個體系。
ASP.NET Core支持多種授權方式,包括兼容之前的角色授權。下面通過幾個例子說明一下(例子依然以上一章的代碼為基礎)。
二、基於角色授權
ASP.NET Core兼容之前的角色授權模式,如何使用呢?由於不是本文的重點,這裡只是簡要說一下。修改FlyLolo.JWT.Server的TokenHelper臨時為張三添加了一個名為“TestPutBookRole”的權限(實際權限來源此處不做展示)。
public ComplexToken CreateToken(User user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) }; //下面對code為001的張三添加了一個Claim,用於測試在Token中存儲用戶的角色信息,對應測試在FlyLolo.JWT.API的BookController的Put方法,若用不到可刪除 if (user.Code.Equals("001")) { claims = claims.Append(new Claim(ClaimTypes.Role, "TestPutBookRole")).ToArray(); } return CreateToken(claims); }
修改FlyLolo.JWT.API的BookController,添加了一個Action如下
/// <summary> /// 測試在JWT的token中添加角色,在此驗證 見TokenHelper /// </summary> /// <returns></returns> [HttpPut] [Authorize(Roles = "TestPutBookRole")] public JsonResult Put() { return new JsonResult("Put Book ..."); }
訪問這個Action,只有用張三登錄後獲取的Token能正常訪問。
三、基於聲明授權
對於上例來說,本質上也是基於聲明(Claim)的授權,因為張三的”TestPutBookRole”角色也是作為一個Claim添加到證書中的。只不過採用了特定的ClaimTypes.Role。那麼是否可以將其他的普通Claim作為授權的依據呢?當然是可以的。
這裡涉及到了另一個單詞“Policy”,翻譯為策略?也就是說,可以把一系列的規則(例如要求姓名為李四,賬號為002,國籍為中國等等)組合在一起,形成一個Policy,只有滿足這個Policy的才可以被授權訪問。
下面我們就新建一個Policy,在Startup的ConfigureServices中添加授權代碼:
services.AddAuthorization(options=>options.AddPolicy("Name",policy=> { policy.RequireClaim(ClaimTypes.Name, "張三"); policy.RequireClaim(ClaimTypes.NameIdentifier,"001"); }));
在BookController中添加一個Action如下
[HttpDelete] [Authorize(Policy = "TestPolicy")] public JsonResult Delete() { return new JsonResult("Delete Book ..."); }
可以通過張三和李四的賬號測試一下,只有使用張三的賬號獲取的Token能訪問成功。
四、基於策略自定義授權
上面介紹了兩種授權方式,現在有個疑問,通過角色授權,只適合一些小型項目,將幾個功能通過角色區分開就可以了。
通過聲明的方式,目測實際項目中需要在Startup中先聲明一系列的Policy,然後在Controller或Action中使用。
這兩種方式都感覺不好。例如經常存在這樣的需求:一個用戶可以有多個角色,每個角色對應多個可訪問的API地址(將授權細化到具體的Action)。用戶還可以被特殊的授予某個API地址的權限。
這樣的需求採用上面的兩種方式實現起來都很麻煩,好在ASP.NET Core提供了方便的擴展方式。
1.樣例數據
將上面的需求匯總一下,最終可以形成如下形式的數據:
/// <summary> /// 虛擬數據,模擬從數據庫或緩存中讀取用戶相關的權限 /// </summary> public static class TemporaryData { public readonly static List<UserPermissions> UserPermissions = new List<UserPermissions> { new UserPermissions { Code = "001", Permissions = new List<Permission> { new Permission { Code = "A1", Name = "student.create", Url = "/api/student",Method="post" }, new Permission { Code = "A2", Name = "student.delete", Url = "/api/student",Method="delete"} } }, new UserPermissions { Code = "002", Permissions = new List<Permission> { new Permission { Code = "B1", Name = "book.create", Url = "/api/book" ,Method="post"}, new Permission { Code = "B2", Name = "book.delete", Url = "/api/book" ,Method="delete"} } }, }; public static UserPermissions GetUserPermission(string code) { return UserPermissions.FirstOrDefault(m => m.Code.Equals(code)); } }
涉及到的兩個類如下:
public class Permission { public string Code { get; set; } public string Name { get; set; } public string Url { get; set; } public string Method { get; set; } } public class UserPermissions { public string Code { get; set; } public List<Permission> Permissions { get; set; } }
2.自定義處理程序
下面就是根據樣例數據來制定相應的處理程序了。這涉及到IAuthorizationRequirement和AuthorizationHandler兩個內容。
IAuthorizationRequirement是一個空的接口,主要用於提供授權所需要滿足的“要求”,或者說是“規則”。AuthorizationHandler則是對請求和“要求”的聯合處理。
新建一個PermissionRequirement實現IAuthorizationRequirement接口。
public class PermissionRequirement: IAuthorizationRequirement { public List<UserPermissions> UsePermissionList { get { return TemporaryData.UserPermissions; } } }
很簡單的內容。它的“要求”也就是用戶的權限列表了,用戶的權限列表中包含當前訪問的API,則授權通過,否則不通過。
判斷邏輯放在新建的PermissionHandler中:
public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { var code = context.User.Claims.FirstOrDefault(m => m.Type.Equals(ClaimTypes.NameIdentifier)); if (null != code) { UserPermissions userPermissions = requirement.UsePermissionList.FirstOrDefault(m => m.Code.Equals(code.Value.ToString())); var Request = (context.Resource as AuthorizationFilterContext).HttpContext.Request; if (null != userPermissions && userPermissions.Permissions.Any(m => m.Url.ToLower().Equals(Request.Path.Value.ToLower()) && m.Method.ToLower().Equals(Request.Method.ToLower()) )) { context.Succeed(requirement); } else { context.Fail(); } } else { context.Fail(); } return Task.CompletedTask; } }
邏輯很簡單不再描述。
3.使用自定義的處理程序
在Startup的ConfigureServices中添加授權代碼
services.AddAuthorization(options => options.AddPolicy("Permission", policy => policy.Requirements.Add(new PermissionRequirement()))); services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
將BookController的Delete Action修改一下:
[HttpDelete] //[Authorize(Policy = "TestPolicy")] [Authorize(Policy = "Permission")] public JsonResult Delete() { return new JsonResult("Delete Book ..."); }
測試一下只有李四可以訪問這個Action。
代碼地址:https://github.com/FlyLolo/JWT.Demo/releases/tag/2.0