1.JWT簡介
官方定義:JWT是JSON Web Token的縮寫,JSON Web Token是一個開放標準(RFC 7519),它定義了一種緊湊的、自包含的方式,可以將各方之間的資訊作為JSON物件安全地傳輸。該資訊可以被驗證和信任,因為它是經過加密的。
實際上,Oauth2.0中的access token一般就是jwt格式。
token由三部分組成,通過"."分隔,分別是:
● 檔頭
● 有效載荷
● 簽名
所以JWT表示為:aaaaa.bbbbb.ccccc組成。
1)檔頭,Header通常由兩部分組成:使用的加密演演算法 "alg" 以及Token的種類 "typ"。如下:
{ "alg": "HS256", "typ": "JWT" }
此JSON被Base64Url編碼以形成JWT的第一部分。
2)有效荷載,Payload主要包含了宣告Claims,宣告實際就是key:value資料,主要包含以下三種宣告:
Registered Claims: 註冊宣告,為IANA JSON Web Token 登入檔中預先定義好的宣告,這些宣告非強制性,但是建議使用,如
● iss(issuer):簽發人
● exp(expiration time) :過期時間
● sub(subject):主題
● aud(audience):受眾
● nbf(not befaore):生效時間
● lat(issued at):簽發時間
● jti(jwt id):編號
Public Claims:公共宣告,名稱可以被任意定義。為了防止重複,任何新的Claim名稱都應該被定義在IANA JSON Web Token Registry中或者使用一個包含不易重複名稱空間的URI。
Private Claims:私有宣告,是在團隊中約定使用的自定義Claims,既不屬於Registered也不屬於Public。
此JSON進行Base64Url編碼形成JWT的第二部分
3)Signature,簽名是將第一部分(header)、第二部分(payload)、金鑰(key)通過指定演演算法(HMAC、RSA)進行加密生成的。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
生成的簽名就是JWT的第三部分。
將這三部分拼接在一起並使用"."分隔後形成的字串就是Token。如:
可以使用jwt.io的Debugger解碼,驗證或生成JWT。
注意:雖然簽名過後的Token可以防止篡改,但是Token的資訊是公開的,任何人都可以讀取,所以儘量不要在有效載荷或檔頭傳遞敏感資訊(如密碼)。
2.使用場景
1)目前來說,幾乎所有之前使用cookie,session的地方,都可以換成jwt。
2)標準的對C或者對B的微服務系統,這個和之前講的Oauth2.0協定最大的不同之處,jwt只是一個傳輸令牌,oauth2.0是一個授權協定,jwt可以理解為是oauth2.0的一部分。。。
3)資訊保安交換,由於簽名防篡改機制,可以驗證其發行人和收件人。
3.Jwt的優勢
1)無需儲存,無伺服器壓力,輕量級使用,簡單上手。
2)無視跨域,可多端使用,不像cookie、session依賴瀏覽器。
4.前端滑動登入狀態管理方案
jwt token的過期時間如果短了,很影響前端使用者操作體驗,所以一般情況都是中長期的,以前的session管理登入狀態,是滑動的,而現在jwt是無狀態的,那麼怎麼才能做到滑動管理呢,具體細節分析請看這篇文章.NET Core WebAPI 認證授權之JWT(四):JWT續期問題 - 不落閣 (leo96.com) ,雖然沒有解決,但是問題丟擲的很細緻,,我來說說我們現在正在使用的方案。
其實也很簡單,使用者請求後端服務資料時,作為有效動作,並且觸發2小時的計時器,沒到2小時的定時器清除並且重新啟動,如果2小時內使用者沒有任何動作,認為是可以退出登入。
5..net core使用jwt
nuget安裝System.IdentityModel.Tokens.Jwt
新建一個service寫一個生成token的方法
public class TokenService : ITokenService { private IConfiguration configuration; public TokenService(IConfiguration configuration) { this.configuration = configuration; } public async Task<string> MakeJwtToken(long userId) { var claims = new[] { // 角色需要在這裡填寫 new Claim(ClaimTypes.Role, "Admin"), // 多個角色可以重複寫,生成的 JWT 會是一個陣列 new Claim(ClaimTypes.Role, "SuperAdmin"), //其他宣告 new Claim("uid", userId.ToString()) }; //私鑰,驗證方也需要使用這個進行驗證。 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Auth:SecurityKey"])); //加密方式 var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: "AESCR",//發行人 audience: "AESCR",//接收人 claims: claims, expires: DateTime.Now.AddMonths(30),//過期時間 signingCredentials: creds); var res = new JwtSecurityTokenHandler().WriteToken(token); return await Task.FromResult<string>(res); } }
啟動類認證注入
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true,//是否驗證發行人 ValidateAudience = true,//是否驗證收件人 ValidateLifetime = true,//是否驗證失效時間 ValidateIssuerSigningKey = true,//是否驗證SecurityKey ValidAudience = "AESCR",//Audience ValidIssuer = "AESCR",//Issuer,這兩項和後面簽發jwt的設定一致 ClockSkew = TimeSpan.Zero, // // 預設允許 300s 的時間偏移量,設定為0 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Auth:SecurityKey"]))//與建立者金鑰一致 }; });
app.UseAuthentication();//新增認證中介軟體
控制器程式碼
/// <summary> /// 編輯使用者資訊 /// </summary> /// <param name="userId"></param> /// <param name="command"></param> /// <returns></returns> [HttpPost("{userId}/edit/userInfo")] [ProducesResponseType(typeof(Users), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] [Authorize]//token認證標籤,如果需要角色認證,[Authorize(Roles ="admin")] public async Task<IActionResult> EditUserInfo([FromRoute] long userId, [FromBody] EditUserInfoCommand command) { if (!this.CheckUser(command.UserId)) return BadRequest("您沒有許可權存取"); var result = await userService.EditUserInfo(command); if (!result.IsSuccess) return BadRequest(result.FailureReason); return Ok(result.GetData()); }
以上程式碼就完事了,簡單吧,在請求的時候,帶上token就可以了。
這裡有一個細節問題,由於我們這裡使用者ID,都是在jwt的Payload中,那麼是否還需要請求介面的時候在引數中傳輸呢?個人理解是這樣:
1.首先先看下token驗證過後,怎麼取claims的宣告
public static class ControllerExtensions { public static long GetUserId(this ControllerBase controllerBase) { var claim = controllerBase.User.Claims.Where(p => p.Type == "uid").FirstOrDefault(); if (claim == null) return 0; long res = 0; long.TryParse(claim.Value, out res); return res; } public static bool CheckUser(this ControllerBase controllerBase, long userId) { return controllerBase.GetUserId() == userId; } }
我可以從payload中獲取到建立token時帶進去的使用者id---uid,回到問題,我認為即使可以拿到使用者ID,也需要從介面引數中傳遞過來,因為安全認證是一個切面攔截,我們的服務如果去掉切面,要保障正常執行,我們只需要在加一個傳遞引數中的uid和宣告裡的uid是否一致,來判斷是否是當前使用者的操作。