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