ASP.NET Core 中jwt授權認證的流程原理
- 2020 年 3 月 15 日
- 筆記
1,快速實現授權驗證
什麼是 JWT ?為什麼要用 JWT ?JWT 的組成?
這些百度可以直接找到,這裡不再贅述。
實際上,只需要知道 JWT 認證模式是使用一段 Token 作為認證依據的手段。
我們看一下 Postman 設置 Token 的位置。
那麼,如何使用 C# 的 HttpClient 訪問一個 JWT 認證的 WebAPI 呢?
下面來創建一個 ASP.NET Core 項目,嘗試添加 JWT 驗證功能。
1.1 添加 JWT 服務配置
在 Startup.cs 的 ConfigureServices
方法中,添加一個服務
// 設置驗證方式為 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
中的中間件
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 中,添加一個這樣的方法
static void ConsoleToke() { // 定義用戶資訊 var claims = new Claim[] { new Claim(ClaimTypes.Name, "痴者工良"), new Claim(JwtRegisteredClaimNames.Email, "66666666666@qq.com"), }; // 和 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()
中,調用此方法
public static void Main(string[] args) { ConsoleToke(); CreateHostBuilder(args).Build().Run(); }
1.3 添加 API訪問
我們添加一個 API。
[Authorize]
特性用於標識此 Controller 或 Action 需要使用合規的 Token 才能登錄。
[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。
發現報 401 (無許可權)狀態碼,這是因為請求時不攜帶令牌,會導致不能訪問 API。
從控制台終端複製生成的 Token 碼,複製到 Postman 中,再次訪問,發現響應狀態碼為 200,響應成功。
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 是否有效。
使用特性設置相應通過認證才能訪問,一般有以下情況。
// 不適用特性,可以直接訪問 public class AController : ControllerBase { public string Get() { return "666"; } } /// <summary> /// 整個控制器都需要授權才能訪問 /// </summary> [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"; } } /// <summary> /// 整個控制器都需要授權,但 Get 不需要 /// </summary> [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 解析。
context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, JwtBearerDefaults.AuthenticationScheme);
那麼,我們如何手工從原生的 Http 請求中,解析出來呢?且看我慢慢來分解步驟。
首先創建一個 TestMiddleware 文件,作為中間件使用。
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 ,自己加個判斷。
貼到程式碼區域。
string tokenStr = context.Request.Headers["Authorization"].ToString();
Header 的 Authorization 鍵,是由 Breaer {Token}
組成的字元串。
2.1.2 判斷是否為有效令牌
拿到 Token 後,還需要判斷這個 Token 是否有效。
因為 Authorization 是由 Breaer {Token}
組成,所以我們需要去掉前面的 Brear
才能獲取 Token。
/// <summary> /// Token是否是符合要求的標準 Json Web 令牌 /// </summary> /// <param name="tokenStr"></param> /// <returns></returns> 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 是否符合協議規範。
將下面判斷貼到程式碼區域。
if (!IsCanReadToken(ref tokenStr)) return ;
2.1.3 解析 Token
下面程式碼可以將 Header 的 Authorization 內容轉為 JwtSecurityToken 對象。
(截取字元串的方式很多種,喜歡哪個就哪個。。。)
/// <summary> /// 從Token解密出JwtSecurityToken,JwtSecurityToken : SecurityToken /// </summary> /// <param name="tokenStr"></param> /// <returns></returns> public JwtSecurityToken GetJwtSecurityToken(string tokenStr) { var jwt = jwtSecurityTokenHandler.ReadJwtToken(tokenStr); return jwt; }
不過這個 GetJwtSecurityToken
不是我們關注的內容,我們是要獲取 Claim。
JwtSecurityToken.Claims
將下面程式碼貼到程式碼區域
JwtSecurityToken jst = GetJwtSecurityToken(tokenStr); IEnumerable<Claim> claims = jst.Claims;
2.1.4 生成 context.User
context.User 是一個 ClaimsPrincipal 類型,我們通過解析出的 Claim,生成 ClaimsPrincipal。
JwtSecurityToken jst = GetJwtSecurityToken(tokenStr); IEnumerable<Claim> claims = jst.Claims; List<ClaimsIdentity> ci = new List<ClaimsIdentity>() { new ClaimsIdentity(claims) }; context.User = new ClaimsPrincipal(ci);
最終的程式碼塊是這樣的
// 我們寫程式碼的區域 string tokenStr = context.Request.Headers["Authorization"].ToString(); string requestUrl = context.Request.Path.Value; if (!IsCanReadToken(ref tokenStr)) return; JwtSecurityToken jst = GetJwtSecurityToken(tokenStr); IEnumerable<Claim> claims = jst.Claims; List<ClaimsIdentity> ci = new List<ClaimsIdentity>() { 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
。
以下程式碼可以獲取所訪問的節點資訊。
var endpoint = context.GetEndpoint();
那麼如何判斷所訪問的 Controller 和 Action 是否使用了認證相關的特性?
var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
Metadata 是一個 ASP.NET Core 實現的集合對象,GetOrderedMetadata<T>
可以找出需要的特性資訊。
這個集合不會區分是 Contrller 還是 Action 的 [Authorize]
特性。
那麼判斷 是否有 [AllowAnonymous]
特性,可以這樣使用。
if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null) { await _next(context); return; }