【什麼是JWT】
JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。
JWT的官網地址:https://jwt.io/
通俗地來講,JWT是能代表使用者身份的令牌,可以使用JWT令牌在api介面中校驗使用者的身份以確認使用者是否有訪問api的許可權。
JWT中包含了身份認證必須的引數以及使用者自定義的引數,JWT可以使用祕密(使用HMAC演算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
【什麼時候應該使用JSON Web令牌?】
-
授權:這是使用JWT的最常見方案。一旦使用者登入,每個後續請求將包括JWT,允許使用者訪問該令牌允許的路由,服務和資源。Single Sign On是一種現在廣泛使用JWT的功能,因為它的開銷很小,並且能夠在不同的域中輕鬆使用。
-
資訊交換:JSON Web令牌是在各方之間安全傳輸資訊的好方法。因為JWT可以簽名 - 例如,使用公鑰/私鑰對 - 您可以確定發件人是他們所說的人。此外,由於使用標頭和有效負載計算簽名,您還可以驗證內容是否未被篡改。
【JWT有什麼優勢?】
我們先看我們傳統的身份校驗方式
- 使用者向伺服器傳送使用者名稱和密碼。
- 伺服器驗證通過後,在當前對話(session)裡面儲存相關資料,比如使用者角色、登入時間等等。
- 伺服器向使用者返回一個 session_id,寫入使用者的 Cookie。
- 使用者隨後的每一次請求,都會通過 Cookie,將 session_id 傳回伺服器。
- 伺服器收到 session_id,找到前期儲存的資料,由此得知使用者的身份。
這種模式的問題在於,擴充套件性(scaling)不好。單機當然沒有問題,如果是伺服器叢集,或者是跨域的服務導向架構,就要求 session 資料共享,每臺伺服器都能夠讀取 session。如果session儲存的節點掛了,那麼整個服務都會癱瘓,體驗相當不好,風險也很高。
相比之下,JWT的實現方式是將使用者資訊儲存在客戶端,服務端不進行儲存。每次請求都把令牌帶上以校驗使用者登入狀態,這樣服務就變成了無狀態的,伺服器叢集也很好擴充套件。
【JWT令牌結構】
在緊湊的形式中,JSON Web Tokens由dot(.
)分隔的三個部分組成,它們是:
- Header 頭
- Payload 有效載荷
- Signature 簽名
因此,JWT通常如下所示:
xxxxx.yyyyy.zzzzz
1.Header 頭
標頭通常由兩部分組成:令牌的型別,即JWT,以及正在使用的簽名演算法,例如HMAC SHA256或RSA。
例如:
{ "alg": "HS256", "typ": "JWT" }
然後,這個JSON被編碼為Base64Url,形成JWT的第一部分。
2.Payload 有效載荷
Payload 部分也是一個 JSON 物件,用來存放實際需要傳遞的資料。JWT 規定了7個官方欄位,供選用。
-
iss (issuer):簽發人
-
exp (expiration time):過期時間
-
sub (subject):主題
-
aud (audience):受眾
-
nbf (Not Before):生效時間
-
iat (Issued At):簽發時間
-
jti (JWT ID):編號
除了官方欄位,你還可以在這個部分定義私有欄位,下面就是一個例子。例如:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
注意,JWT 預設是不加密的,任何人都可以讀到,所以不要把祕密資訊放在這個部分。這個 JSON 物件也要使用 Base64URL 演算法轉成字串。
3.Signature 簽名
Signature 部分是對前兩部分的簽名,防止資料篡改。
首先,需要指定一個金鑰(secret)。這個金鑰只有伺服器才知道,不能洩露給使用者。然後,使用 Header 裡面指定的簽名演算法(預設是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
簽名用於驗證訊息在此過程中未被更改,並且,在使用私鑰簽名的令牌的情況下,它還可以驗證JWT的發件人是否是它所聲稱的人。
把他們三個全部放在一起
輸出是三個由點分隔的Base64-URL字串,可以在HTML和HTTP環境中輕鬆傳遞,而與基於XML的標準(如SAML)相比更加緊湊。
下面顯示了一個JWT,它具有先前的頭和有效負載編碼,並使用機密簽名。
如果您想使用JWT並將這些概念付諸實踐,您可以使用jwt.io Debugger來解碼,驗證和生成JWT。
【JSON Web令牌如何工作?】
在身份驗證中,當使用者使用其憑據成功登入時,將返回JSON Web令牌。由於令牌是憑證,因此必須非常小心以防止出現安全問題。一般情況下,您不應該將令牌保留的時間超過要求。
每當使用者想要訪問受保護的路由或資源時,使用者代理應該使用承載模式傳送JWT,通常在Authorization標頭中。標題的內容應如下所示:
Authorization: Bearer <token>
在某些情況下,這可以是無狀態授權機制。伺服器的受保護路由將檢查Authorization
標頭中的有效JWT ,如果存在,則允許使用者訪問受保護資源。如果JWT包含必要的資料,則可以減少查詢資料庫以進行某些操作的需要,儘管可能並非總是如此。
如果在標Authorization
頭中傳送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。
下圖顯示瞭如何獲取JWT並用於訪問API或資源:
- 應用程式向授權伺服器請求授權
- 校驗使用者身份,校驗成功,返回token
- 應用程式使用訪問令牌訪問受保護的資源
【ASP.Net Core 整合JWT】
前面我們介紹了JWT的原理,下面我們在asp.net core實際專案中整合JWT。
首先我們新建一個Demo asp.net core 空web專案
新增資料訪問模擬api,ValuesController
其中api/value1是可以直接訪問的,api/value2新增了許可權校驗特性標籤 [Authorize]
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Demo.Jwt.Controllers { [ApiController] public class ValuesController : ControllerBase { [HttpGet] [Route("api/value1")] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value1" }; } [HttpGet] [Route("api/value2")] [Authorize] public ActionResult<IEnumerable<string>> Get2() { return new string[] { "value2", "value2" }; } } }
新增模擬登陸,生成Token的api,AuthController
這裡模擬一下登陸校驗,只驗證了使用者密碼不為空即通過校驗,真實環境完善校驗使用者和密碼的邏輯。
using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Demo.Jwt.Controllers { [Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { [AllowAnonymous] [HttpGet] public IActionResult Get(string userName, string pwd) { if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(pwd)) { var claims = new[] { new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.Name, userName) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: Const.Domain, audience: Const.Domain, claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } else { return BadRequest(new { message = "username or password is incorrect." }); } } } }
Startup新增JWT驗證的相關配置
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; using System.Text; namespace Demo.Jwt { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //新增jwt驗證: services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true,//是否驗證Issuer ValidateAudience = true,//是否驗證Audience ValidateLifetime = true,//是否驗證失效時間 ClockSkew = TimeSpan.FromSeconds(30), ValidateIssuerSigningKey = true,//是否驗證SecurityKey ValidAudience = Const.Domain,//Audience ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設定一致 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey }; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ///新增jwt驗證 app.UseAuthentication(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } }
最後把程式碼裡面用到的一些相關常量也貼上過來,Const.cs
namespace Demo.Jwt { public class Const { /// <summary> /// 這裡為了演示,寫死一個金鑰。實際生產環境可以從配置檔案讀取,這個是用網上工具隨便生成的一個金鑰 /// </summary> public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB"; public const string Domain = "http://localhost:5000"; } }
到這裡,已經是我們專案的所有程式碼了。
如果需要完整的專案程式碼,Github地址:https://github.com/sevenTiny/Demo.Jwt
【JWT測試】
我們找一個趁手的工具,比如fiddler,然後把我們的web站點執行起來
首先呼叫無許可權的介面:http://localhost:5000/api/value1
正確地返回了資料,那麼接下來我們測試JWT的流程
1. 無許可權
首先我們什麼都不加呼叫介面:http://localhost:5000/api/value2
返回了狀態碼401,也就是未經授權:訪問由於憑據無效被拒絕。 說明JWT校驗生效了,我們的介面收到了保護。
2.獲取Token
呼叫模擬登陸授權介面:http://localhost:5000/api/Auth?userName=zhangsan&pwd=123
這裡的使用者密碼是隨便寫的,因為我們模擬登陸只是校驗了下非空,因此寫什麼都能通過
成功得到了響應
然後我們得到了一個xxx.yyy.zzz 格式的 token 值。我們把token複製出來
3.在剛才401的介面請求HEADER中新增JWT的引數,把我們的token加上去
再次呼叫我們的模擬資料介面,但是這次我們加了一個HEADER:http://localhost:5000/api/value2
把內容粘出來
User-Agent: Fiddler Host: localhost:5000 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzQ1MDIxIiwiZXhwIjoxNTYwMzQ2ODIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x7Slk4ho1hZc8sR8_McVTB6VEYLz_v-5eaHvXtIDS-o
這裡需要注意 Bearer 後面是有一個空格的,然後就是我們上一步獲取到的token
嗯,沒有401了,成功返回了資料
4.JWT的Token過期
我們且倒一杯開水,坐等30分鐘(我們程式碼中設定的過期時間),然後再次呼叫資料介面:http://localhost:5000/api/value2
又變成了401,我們看下詳細的返回資料
這裡有標註,錯誤描述 token過期,說明我們設定的token過期時間生效了
5.JWT新增自定義的引數(比如帶上使用者資訊)
假如我們想在認證通過的時候,直接從jwt的token中獲取到登陸的使用者名稱,該怎麼操作呢?
首先在我們的獲取token 的api介面裡面新增一個Claim節點,key可以隨便給,也可以使用已經提供好的一些預置Key,value是我們登陸的userName(僅作為演示)
然後在我們的模擬資料介面獲取自定義引數
這裡使用HttpContext的授權擴充套件方法,拿到認證的資訊,我們來看下結果
請求成功返回,並且也拿到了我們一開始寫入的userName
【結束】
到這裡,我們JWT的簡介以及asp.net core 整合JWT已經完美完成,當然了這只是一個demo,在實際的應用中需要補充和完善的地方還有很多。
如果想要完整專案原始碼的,可以參考地址:https://github.com/sevenTiny/Demo.Jwt
如果有幸能幫助到你,高抬貴手點個star吧~