ASP.NET Core 中jwt授權認證的流程原理
阿新 • • 發佈:2020-03-15
[TOC]
## 1,快速實現授權驗證
什麼是 JWT ?為什麼要用 JWT ?JWT 的組成?
這些百度可以直接找到,這裡不再贅述。
實際上,只需要知道 JWT 認證模式是使用一段 Token 作為認證依據的手段。
我們看一下 Postman 設定 Token 的位置。
![](https://img2020.cnblogs.com/blog/1315495/202003/1315495-20200315145237288-1522687558.png)
那麼,如何使用 C# 的 HttpClient 訪問一個 JWT 認證的 WebAPI 呢?
![](https://img2020.cnblogs.com/blog/1315495/202003/1315495-20200315145247048-773187380.png)
下面來建立一個 ASP.NET Core 專案,嘗試新增 JWT 驗證功能。
### 1.1 新增 JWT 服務配置
在 Startup.cs 的 `ConfigureServices` 方法中,新增一個服務
```C#
// 設定驗證方式為 Bearer Token
// 你也可以新增 using Microsoft.AspNetCore.Authentication.JwtBearer;
// 使用 JwtBearerDefaults.AuthenticationScheme 代替 字串 "Brearer"
services.AddAuthentication("Bearer")
.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)
};
});
```
修改 `Configure` 中的中介軟體
```C#
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication(); // 注意這裡
app.UseAuthorization();
```
就是這麼簡單,通過以上設定,要求驗證請求是否有許可權。
### 1.2 頒發 Token
頒發的 Token ,ASP.NET Core 不會儲存。
ASP.NET Core 啟用了 Token 認證,你隨便將生成 Token 的程式碼放到不同程式的控制檯,只要金鑰和 Issuer 和 Audience 一致,生成的 Token 就可以登入這個 ASP.NET Core。
也就是說,可以隨意建立控制檯程式生成 Token,生成的 Token 完全可以登入 ASP.NET Core 程式。
至於原因,我們後面再說,
在 Program.cs 中,新增一個這樣的方法
```C#
static void ConsoleToke()
{
// 定義使用者資訊
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, "痴者工良"),
new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
};
// 和 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);
Console.WriteLine(jwtToken);
}
```
`Main()` 中,呼叫此方法
```C#
public static void Main(string[] args)
{
ConsoleToke();
CreateHostBuilder(args).Build().Run();
}
```
### 1.3 新增 API訪問
我們新增一個 API。
`[Authorize]` 特性用於標識此 Controller 或 Action 需要使用合規的 Token 才能登入。
```C#
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class HomeController : ControllerBase
{
public string Get()
{
Console.WriteLine(User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name));
return "訪問成功";
}
}
```
然後啟動 ASP.NET Core,在 Postman 測試 訪問 https://localhost/api/home。
![](https://img2020.cnblogs.com/blog/1315495/202003/1315495-20200315145311894-1355256906.png)
發現報 401 (無許可權)狀態碼,這是因為請求時不攜帶令牌,會導致不能訪問 API。
從控制檯終端複製生成的 Token 碼,複製到 Postman 中,再次訪問,發現響應狀態碼為 200,響應成功。
![](https://img2020.cnblogs.com/blog/1315495/202003/1315495-20200315145325988-376843745.png)
ASP.NET Core 自帶 jwt 認證大概就是這樣。
那麼,ASP.NET Core 內部是如何實現的呢?又有哪些特性哪些坑呢?請往下看~
## 2,探究授權認證中介軟體
在上面的操作中,我們在管道配置了兩個中介軟體。
```
app.UseAuthentication();
app.UseAuthorization();
```
`app.UseAuthentication();` 的作用是通過 ASP.NET Core 中配置的授權認證,讀取客戶端中的身份標識(Cookie,Token等)並解析出來,儲存到 `context.User` 中。
`app.UseAuthorization();` 的作用是判斷當前訪問 `Endpoint` (Controller或Action)是否使用了 `[Authorize]`以及配置角色或策略,然後校驗 Cookie 或 Token 是否有效。
使用特性設定相應通過認證才能訪問,一般有以下情況。
```C#
// 不適用特性,可以直接訪問
public class AController : ControllerBase
{
public string Get() { return "666"; }
}
///
/// 整個控制器都需要授權才能訪問
///
[Authorize]
public class BController : ControllerBase
{
public string Get() { return "666"; }
}
public class CController : ControllerBase
{
// 只有 Get 需要授權
[Authorize]
public string Get() { return "666"; }
public string GetB() { return "666"; }
}
///
/// 整個控制器都需要授權,但 Get 不需要
///
[Authorize]
public class DController : ControllerBase
{
[AllowAnonymous]
public string Get() { return "666"; }
}
```
### 2.1 實現 Token 解析
至於 ASP.NET Core 中,`app.UseAuthentication();` 和 `app.UseAuthorization();` 的原始碼各種使用了一個專案來寫,程式碼比較多。要理解這兩個中介軟體的作用,我們不妨來手動實現他們的功能。
解析出的 Token 是一個 ClaimsPrincipal 物件,將此物件給 `context.User` 賦值,然後在 API 中可以使用 `User` 例項來獲取使用者的資訊。
在中介軟體中,使用下面的程式碼可以獲取客戶端請求的 Token 解析。
```C#
context.RequestServices.GetRequiredService().AuthenticateAsync(context, JwtBearerDefaults.AuthenticationScheme);
```
那麼,我們如何手工從原生的 Http 請求中,解析出來呢?且看我慢慢來分解步驟。
首先建立一個 TestMiddleware 檔案,作為中介軟體使用。
```C#
public class TestMiddleware
{
private readonly RequestDelegate _next;
jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
public TestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// 我們寫程式碼的區域
// 我們寫程式碼的區域
await _next(context);
}
}
```
#### 2.1.1 從 Http 中獲取 Token
下面程式碼可以中 http 請求中,取得頭部的 Token 。
當然,客戶端可能沒有攜帶 Token,可能獲取結果為 null ,自己加個判斷。
貼到程式碼區域。
```c#
string tokenStr = context.Request.Headers["Authorization"].ToString();
```
Header 的 Authorization 鍵,是由 `Breaer {Token} `組成的字串。
#### 2.1.2 判斷是否為有效令牌
拿到 Token 後,還需要判斷這個 Token 是否有效。
因為 Authorization 是由 `Breaer {Token}`組成,所以我們需要去掉前面的 `Brear ` 才能獲取 Token。
```c#
///
/// Token是否是符合要求的標準 Json Web 令牌
///
///
///
public bool IsCanReadToken(ref string tokenStr)
{
if (string.IsNullOrWhiteSpace(tokenStr) || tokenStr.Length < 7)
return false;
if (!tokenStr.Substring(0, 6).Equals(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme))
return false;
tokenStr = tokenStr.Substring(7);
bool isCan = jwtSecurityTokenHandler.CanReadToken(tokenStr);
return isCan;
}
```
獲得 Token 後,通過 `JwtSecurityTokenHandler.CanReadToken(tokenStr);` 來判斷 Token 是否符合協議規範。
將下面判斷貼到程式碼區域。
```C#
if (!IsCanReadToken(ref tokenStr))
return ;
```
#### 2.1.3 解析 Token
下面程式碼可以將 Header 的 Authorization 內容轉為 JwtSecurityToken 物件。
(擷取字串的方式很多種,喜歡哪個就哪個。。。)
```c#
///
/// 從Token解密出JwtSecurityToken,JwtSecurityToken : SecurityToken
///
///
///
public JwtSecurityToken GetJwtSecurityToken(string tokenStr)
{
var jwt = jwtSecurityTokenHandler.ReadJwtToken(tokenStr);
return jwt;
}
```
不過這個 `GetJwtSecurityToken` 不是我們關注的內容,我們是要獲取 Claim。
```
JwtSecurityToken.Claims
```
將下面程式碼貼到程式碼區域
```c#
JwtSecurityToken jst = GetJwtSecurityToken(tokenStr);
IEnumerable claims = jst.Claims;
```
#### 2.1.4 生成 context.User
context.User 是一個 ClaimsPrincipal 型別,我們通過解析出的 Claim,生成 ClaimsPrincipal。
```C#
JwtSecurityToken jst = GetJwtSecurityToken(tokenStr);
IEnumerable claims = jst.Claims;
List ci = new List() { new ClaimsIdentity(claims) };
context.User = new ClaimsPrincipal(ci);
```
最終的程式碼塊是這樣的
```C#
// 我們寫程式碼的區域
string tokenStr = context.Request.Headers["Authorization"].ToString();
string requestUrl = context.Request.Path.Value;
if (!IsCanReadToken(ref tokenStr))
return;
JwtSecurityToken jst = GetJwtSecurityToken(tokenStr);
IEnumerable claims = jst.Claims;
List ci = new List() { new ClaimsIdentity(claims) };
context.User = new ClaimsPrincipal(ci);
var x = new ClaimsPrincipal(ci);
// 我們寫程式碼的區域
```
### 2.2 實現校驗認證
`app.UseAuthentication();` 的大概實現過程已經做出了說明,現在我們來繼續實現 `app.UseAuthorization();` 中的功能。
繼續使用上面的中介軟體,在原始碼塊區域新增新的區域。
```
// 我們寫程式碼的區域
// 我們寫的程式碼塊 2
```
#### 2.2.1 Endpoint
Endpoint 標識了一個 http 請求所訪問的路由資訊和 Controller 、Action 及其特性等資訊。
`[Authorize]` 特性繼承了 `IAuthorizeData`。`[AllowAnonymous]` 特性繼承了 `IAllowAnonymous`。
以下程式碼可以獲取所訪問的節點資訊。
```c#
var endpoint = context.GetEndpoint();
```
那麼如何判斷所訪問的 Controller 和 Action 是否使用了認證相關的特性?
```c#
var authorizeData = endpoint?.Metadata.GetOrderedMetadata() ?? Array.Empty();
```
Metadata 是一個 ASP.NET Core 實現的集合物件,`GetOrderedMetadata` 可以找出需要的特性資訊。
這個集合不會區分是 Contrller 還是 Action 的 `[Authorize]` 特性。
那麼判斷 是否有 `[AllowAnonymous]` 特性,可以這樣使用。
```c#
if (endpoint?.Metadata.GetMetadata() != null)
{
await _next(context);
return;