ASP.NET Core 6.0 新增 JWT 認證和授權

蘆薈柚子茶發表於2022-04-16

序言

本文將分別介紹 Authentication(認證) 和 Authorization(授權)。

並以簡單的例子在 ASP.NET Core 6.0 的 WebAPI 中分別實現這兩個功能。


相關名詞

Authentication 和 Authorization 長得很像,傻傻分不清楚。

Authentication(認證):標識使用者的身份,一般發生在登入的時候。

Authorization(授權):授予使用者許可權,指定使用者能訪問哪些資源;授權的前提是知道這個使用者是誰,所以授權必須在認證之後。


認證(Authentication)

基本步驟

  1. 安裝相關 Nuget 包:Microsoft.AspNetCore.Authentication.JwtBearer
  2. 準備配置資訊(金鑰等)
  3. 新增服務
  4. 呼叫中介軟體
  5. 實現一個 JwtHelper,用於生成 Token
  6. 控制器限制訪問(新增 Authorize 標籤)

1 安裝 Nuget 包

安裝 Microsoft.AspNetCore.Authentication.JwtBearer

在程式包管理器控制檯中:

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 6.0.1

2 準備配置資訊

在 appsetting.json 中,新增一個 Jwt 節點

"Jwt": {
    "SecretKey": "lisheng741@qq.com",
    "Issuer": "WebAppIssuer",
    "Audience": "WebAppAudience"
}

3 新增服務

在 Program.cs 檔案中註冊服務。

// 引入所需的名稱空間
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

// ……
var configuration = builder.Configuration;

// 註冊服務
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true, //是否驗證Issuer
        ValidIssuer = configuration["Jwt:Issuer"], //發行人Issuer
        ValidateAudience = true, //是否驗證Audience
        ValidAudience = configuration["Jwt:Audience"], //訂閱人Audience
        ValidateIssuerSigningKey = true, //是否驗證SecurityKey
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"])), //SecurityKey
        ValidateLifetime = true, //是否驗證失效時間
        ClockSkew = TimeSpan.FromSeconds(30), //過期時間容錯值,解決伺服器端時間不同步問題(秒)
        RequireExpirationTime = true,
    };
});

4 呼叫中介軟體

呼叫 UseAuthentication(認證),必須在所有需要身份認證的中介軟體前呼叫,比如 UseAuthorization(授權)。

// ……
app.UseAuthentication();
app.UseAuthorization();
// ……

5 JwtHelper 類實現

主要是用於生成 JWT 的 Token。

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace TestWebApi;

public class JwtHelper
{
    private readonly IConfiguration _configuration;

    public JwtHelper(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string CreateToken()
    {
        // 1. 定義需要使用到的Claims
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "u_admin"), //HttpContext.User.Identity.Name
            new Claim(ClaimTypes.Role, "r_admin"), //HttpContext.User.IsInRole("r_admin")
            new Claim(JwtRegisteredClaimNames.Jti, "admin"),
            new Claim("Username", "Admin"),
            new Claim("Name", "超級管理員")
        };

        // 2. 從 appsettings.json 中讀取SecretKey
        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));

        // 3. 選擇加密演算法
        var algorithm = SecurityAlgorithms.HmacSha256;

        // 4. 生成Credentials
        var signingCredentials = new SigningCredentials(secretKey, algorithm);

        // 5. 根據以上,生成token
        var jwtSecurityToken = new JwtSecurityToken(
            _configuration["Jwt:Issuer"],     //Issuer
            _configuration["Jwt:Audience"],   //Audience
            claims,                          //Claims,
            DateTime.Now,                    //notBefore
            DateTime.Now.AddSeconds(30),    //expires
            signingCredentials               //Credentials
        );

        // 6. 將token變為string
        var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);

        return token;
    }
}

該 JwtHelper 依賴於 IConfiguration(為了讀取配置檔案),將 JwtHelper 的建立交由 DI 容器,在 Program.cs 中新增服務:

var configuration = builder.Configuration;
builder.Services.AddSingleton(new JwtHelper(configuration));

將 JwtHelper 註冊為單例模式。

6 控制器配置

新建一個 AccountController,以建構函式方式注入 JwtHelper,新增兩個 Action:GetToken 用於獲取 Token,GetTest 打上 [Authorize] 標籤用於驗證認證。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace TestWebApi.Controllers;

[Route("api/[controller]/[action]")]
[ApiController]
public class AccountController : ControllerBase
{
    private readonly JwtHelper _jwtHelper;

    public AccountController(JwtHelper jwtHelper)
    {
        _jwtHelper = jwtHelper;
    }

    [HttpGet]
    public ActionResult<string> GetToken()
    {
        return _jwtHelper.CreateToken();
    }

    [Authorize]
    [HttpGet]
    public ActionResult<string> GetTest()
    {
        return "Test Authorize";
    }
}

7 測試呼叫

方式一:通過 Postman、Apifox 等介面除錯軟體除錯

使用 Postman 呼叫 /api/Account/GetToken 生成 Token

在呼叫 /api/Account/GetTest 時傳入 Token,得到返回結果

方式二:在瀏覽器控制檯除錯

除錯 /api/Account/GetToken

var xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", function() {
   if(this.readyState === 4) {
      console.log(token = this.responseText); //這裡用了一個全域性變數 token,為下一個介面服務
   }
});
xhr.open("GET", "/api/Account/GetToken");
xhr.send();

除錯 /api/Account/GetTest

var xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", function() {
   if(this.readyState === 4) {
      console.log(this.status, this.responseText); //this.status為響應狀態碼,401為無認證狀態
   }
});
xhr.open("GET", "/api/Account/GetTest");
xhr.setRequestHeader("Authorization",`Bearer ${token}`); //附帶上 token
xhr.send();

授權(Authorization)

注意:授權必須基於認證,即:若沒有完成上文關於認證的配置,則下面的授權是不會成功的。

授權部分,將先介紹相關標籤、授權方式,再介紹基於策略的授權。這三部分大致的內容如下描述:

相關標籤:Authorize 和 AllowAnonymous

授權方式:介紹 Policy、Role、Scheme 的基本內容

基於策略(Policy)的授權:深入 Policy 授權方式

相關標籤(Attribute)

授權相關標籤具體請查考官方文件簡單授權

[Authorize]

打上該標籤的 Controller 或 Action 必須經過認證,且可以標識需要滿足哪些授權規則。

授權規則可以是 Policy(策略)、Roles(角色) 或 AuthenticationSchemes(方案)。

[Authorize(Policy = "", Roles ="", AuthenticationSchemes ="")]

[AllowAnonymous]

允許匿名訪問,級別高於 [Authorize] ,若兩者同時作用,將生效 [AllowAnonymous]

授權方式

基本上授權只有:Policy、Role、Scheme 這3種方式,對應 Authorize 標籤的3個屬性。

1 Policy(策略)

推薦的授權方式,在 ASP.NET Core 的官方文件提及最多的。一個 Policy 可以包含多個要求(要求可能是 Role 匹配,也可能是 Claims 匹配,也可能是其他方式。)

下面舉個基礎例子(說是基礎例子,主要是基於 Policy 的授權方式可以不斷深入追加一些配置):

在 Program.cs 中,新增兩條 Policy:

policy1 要求使用者擁有一個 Claim,其 ClaimType 值為 EmployeeNumber。

policy2 要求使用者擁有一個 Claim,其 ClaimType 值為 EmployeeNumber,且其 ClaimValue 值為1、2、3、4 或 5。

builder.Services.AddAuthorization(options => {
    options.AddPolicy("policy1", policy => policy.RequireClaim("EmployeeNumber"));
    options.AddPolicy("policy2", policy => policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
})

在控制器中新增 [Authorize] 標籤即可生效:

[Authorize(Policy = "policy1")]
public class TestController : ControllerBase

或在控制器的 Action 上:

public class TestController : ControllerBase
{
    [Authorize(Policy = "policy1")]
    public ActionResult<string> GetTest => "GetTest";
}

2 Role(角色)

基於角色授權,只要使用者擁有角色,即可通過授權驗證。

在認證時,給使用者新增角色相關的 Claim ,即可標識使用者擁有的角色(注:一個使用者可以擁有多個角色的 Claim),如:

new Claim(ClaimTypes.Role, "admin"),
new Claim(ClaimTypes.Role, "user")

在 Controller 或 Action 中:

[Authorize(Roles = "user")]
public class TestController : ControllerBase
{
    public ActionResult<string> GetUser => "GetUser";
    
    [Authorize(Roles = "admin")] //與控制器的Authorize疊加作用,除了擁有user,還需擁有admin
    public ActionResult<string> GetAdmin => "GetAdmin";
    
    [Authorize(Roles = "user,admin")] //user 或 admin 其一滿足即可
    public ActionResult<string> GetUserOrAdmin => "GetUserOrAdmin";
}

3 Scheme(方案)

方案如:Cookies 和 Bearer,當然也可以是自定義的方案。

由於這種方式不常用,這裡不做展開,請參考官方文件按方案限制標識

基於策略(Policy)的授權

上面已經提及了一個基於策略授權的基礎例子,下面將繼續深入這種授權方式。

1 授權過程

在不斷深入 Policy 這種方式的授權之前,有必要將授權的過程描述一下。授權過程描述建議結合原始碼檢視,這樣能更清楚其中的作用。當然,這一部分是比較難懂,筆者表述可能也不夠清晰,而這一部分對於完成授權的配置也不會有影響,故而如果讀者看不明白或無法理解,可以暫且跳過,不必糾結。

建議看一下ASP.NET Core 認證與授權6:授權策略是怎麼執行的?這篇文章,文章將授權相關的原始碼整理出來了,並說明了其中的關係。

這裡簡單梳理一下:

與授權相關的 interface 和 class 如下:

IAuthorizationService #驗證授權的服務,主要方法AuthorizeAsync
DefaultAuthorizationService #IAuthorizationService的預設實現
IAuthorizationHandler #負責檢查是否滿足要求,主要方法HandleAsync
IAuthorizationRequirement #只有屬性,沒有方法;用於標記服務,以及用於追蹤授權是否成功的機制。
AuthorizationHandler<TRequirement> #主要方法HandleRequirementAsync

這些 interface 和 class 的關係以及授權過程是這樣的:

DefaultAuthorizationService 實現 IAuthorizationServiceAuthorizeAsync 方法。

AuthorizeAsync 方法會獲取到所有實現了 IAuthorizationHandler 的例項,並迴圈呼叫所有例項的 HandleAsync 方法檢查是否滿足授權要求,如果有任一一個 HandleAsync 返回了 Fail 則將結束迴圈(細節請參考官方文件處理程式返回結果),並禁止使用者訪問。

IAuthorizationHandler 的作用如上一點所述,提供了一個 HandleAsync 方法,用於檢查授權。

IAuthorizationRequirement 是一個要求,主要是配合 AuthorizationHandler<TRequirement> 使用。

AuthorizationHandler<TRequirement> 實現了 IAuthorizationHandlerHandleAsync 方法,並提供了一個 HandleRequirementAsync 的方法。HandleRequirementAsync 用於檢查 Requirement(要求)是否滿足。HandleAsync 的預設實現為獲取所有實現 TRequirement 的請求(且該請求由 Policy 新增進列表裡),迴圈呼叫 HandleRequirementAsync,檢查哪個要求(Requirement)能滿足授權。

簡述一下:

[Authorize] 標籤生效時,呼叫的是 IAuthorizationServiceAuthorizeAsync(由 DefaultAuthorizationService 實現)。

AuthorizeAsync 會去呼叫所有 IAuthorizationHandlerHandleAsync (由 AuthorizationHandler<TRequirement> 實現)。

HandleAsync 會去呼叫 AuthorizationHandler<TRequirement>HandleRequirementAsync 的方法。

注意:這裡只是列出了主要的介面和類,部分沒有列出,如:IAuthorizationHandlerProvider(這個介面的預設實現 DefaultAuthorizationHandlerProvider,主要是用於收集 IAuthorizationHandler 並返回 IEnumerable<IAuthorizationHandler>

2 實現說明

IAuthorizationService 已預設實現,不需要我們做額外工作。

IAuthorizationHandlerAuthorizationHandler<TRequirement> 實現。

所以我們要做的,是:

第一步,準備 Requirement 實現 IAuthorizationRequirement

第二步,新增一個 Handler 程式繼承 AuthorizationHandler<TRequirement> 並重寫 HandleRequirementAsync 方法

關於具體實現,可以參考ASP.NET Core 認證與授權7:動態授權基於許可權的授權部分,該文章思路已十分清晰,這裡將其主要步驟列出來。

3 定義許可權項

在實現 Requirement 之前,我們需要先定義一些許可權項,主要用於後續作為 Policy 的名稱,並傳入 我們實現的 Requirement 之中。

public static class UserPermission
{
    public const string User = "User";
    public const string UserCreate = User + ".Create";
    public const string UserDelete = User + ".Delete";
    public const string UserUpdate = User + ".Update";
}

如上,定義了“增”、“刪”、“改”等許可權,其中 User 將擁有完整許可權。

4 實現 Requirement

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    public PermissionAuthorizationRequirement(string name)
    {
        Name = name;
    }
    public string Name { get; set; }
}

使用 Name 屬性表示許可權的名稱,與 UserPermission 中的常量對應。

5 實現授權處理程式 Handler

這裡假定使用者的 Claim 中 ClaimType 為 Permission 的項,如:

new Claim("Permission", UserPermission.UserCreate),
new Claim("Permission", UserPermission.UserUpdate)

如上,標識該使用者使用者 UserCreate 和 UserUpdate 的許可權。

注意:當然,實際程式我們肯定不是這樣實現的,這裡只是簡易示例。

接著,實現一個授權 Handler:

public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
    {
        var permissions = context.User.Claims.Where(_ => _.Type == "Permission").Select(_ => _.Value).ToList();
        if (permissions.Any(_ => _.StartsWith(requirement.Name)))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

執行 HandleRequirementAsync 時,會將使用者的 Claim 中 ClaimType 為 Permission 的項取出,並獲取其 Value 組成一個 List<string>

接著驗證 Requirement 是否滿足授權,滿足則執行 context.Succeed

6 新增授權處理程式

在 Program.cs 中,將 PermissionAuthorizationHandler 新增到 DI 中:

builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

7 新增授權策略

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(UserPermission.UserCreate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserCreate)));
    options.AddPolicy(UserPermission.UserUpdate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserUpdate)));
    options.AddPolicy(UserPermission.UserDelete, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserDelete)));
});

8 控制器配置

控制器如下:

[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
    [HttpGet]
    [Authorize(UserPermission.UserCreate)]
    public ActionResult<string> UserCreate() => "UserCreate";

    [HttpGet]
    [Authorize(UserPermission.UserUpdate)]
    public ActionResult<string> UserUpdate() => "UserUpdate";

    [HttpGet]
    [Authorize(UserPermission.UserDelete)]
    public ActionResult<string> UserDelete() => "UserDelete";
}

基於上面的假定,使用者訪問介面的情況如下:

/api/User/UserCreate #成功
/api/User/UserUpdate #成功
/api/User/UserDelete #403無許可權

至此,基於策略(Policy)的授權其實已經基本完成

接下去的內容,將是對上面一些內容的完善或補充。

完善:實現策略提供程式 PolicyProvider

一般新增授權策略如下是在 Program.cs 中,方式如下:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("policy", policy => policy.RequireClaim("EmployeeNumber"));
});

通過 AuthorizationOptions.AddPolicy 新增授權策略這種方式不靈活,無法動態新增。

通過實現 IAuthorizationPolicyProvider 並新增到 DI 中,可以實現動態新增 Policy。

IAuthorizationPolicyProvider 的預設實現為 DefaultAuthorizationPolicyProvider

實現一個 PolicyProvider 如下:

public class TestAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider, IAuthorizationPolicyProvider
{
    public Test2AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) {}

    public new Task<AuthorizationPolicy> GetDefaultPolicyAsync()
        => return base.GetDefaultPolicyAsync();

    public new Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
        return base.GetFallbackPolicyAsync();

    public new Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith(UserPermission.User))
        {
            var policy = new AuthorizationPolicyBuilder("Bearer");
            policy.AddRequirements(new PermissionAuthorizationRequirement(policyName));
            return Task.FromResult<AuthorizationPolicy?>(policy.Build());
        }
        return base.GetPolicyAsync(policyName);
    }
}

注意:自定義的 TestAuthorizationPolicyProvider 必須實現 IAuthorizationPolicyProvider,否則新增到 DI 時會不生效。

在 Program.cs 中,將自定義的 PolicyProvider 新增到 DI 中:

builder.Services.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();

注意:只會生效最後一個新增的 PolicyProvider。

補充:自定義 AuthorizationMiddleware

自定義 AuthorizationMiddleware 可以:

  • 返回自定義的響應
  • 增強(或者說改變)預設的 challenge 或 forbid 響應

具體請檢視官方文件自定義 AuthorizationMiddleware 的行為

補充:MiniApi 的授權

在 MiniApi 中幾乎都是形如 MapGet() 的分支節點,這類終結點無法使用 [Authorize] 標籤,可以用使用 RequireAuthorization("Something") 進行授權,如:

app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");

參考來源

ASP.NET Core 6.0 官方文件:授權策略提供程式

.NET 6 使用JWT Bearer認證和授權的步驟

ASP.NET Core 認證與授權6:授權策略是怎麼執行的?(mark:這篇很強!)

ASP.NET Core 認證與授權7:動態授權

相關文章