上一章分享了如何在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