javascript
第九节:JWT简介和以JS+WebApi为例基于JWT的安全校验
一. 簡介
1. 背景
傳統(tǒng)的基于Session的校驗(yàn)存在諸多問題,比如:Session過期、服務(wù)器開銷過大、不能分布式部署、不適合前后端分離的項目。 傳統(tǒng)的基于Token的校驗(yàn)需要存儲Key-Value信息,存在Session或數(shù)據(jù)庫中都有弊端,如果按照一定規(guī)律采用對稱加密算法生成token,雖然能解決上面問題,但是一旦對稱加密算法泄露,很容被反編譯;所以在此基礎(chǔ)上繼續(xù)升級,利用userId生成Token,只要保存好秘鑰即可,從而引出JWT。
2. 什么是JWT
Json web token 簡稱:JWT, 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)。該token被設(shè)計為緊湊且安全的,特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該token也可直接被用于認(rèn)證,也可被加密。
下面就是一段JWT字符串(后面詳細(xì)分析)
1 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U3. JWT的優(yōu)點(diǎn)
(1). JWT是無狀態(tài)的,不需要服務(wù)器端保存會話信息,減輕服務(wù)器端的讀取壓力(存儲在客戶端上),同時易于擴(kuò)展、易于分布式部署。
(2). JWT可以跨語言支持。
(3). 便于傳輸,jwt的構(gòu)成很簡單,字節(jié)占用空間少,所以是非常便于傳輸?shù)摹?/p>
(4). 自身構(gòu)成有payload部分,可以存儲一下業(yè)務(wù)邏輯相關(guān)的非敏感信息。
特別聲明:JWT最大的優(yōu)勢是無狀態(tài)的,相對傳統(tǒng)的Session驗(yàn)證能減輕服務(wù)器端的存儲壓力,安全性更高,但也不是絕對的,比如針對同一個接口,JWT字符串被截取后,且在有效期內(nèi),在不篡改JWT字符串的情況下,也是可以模擬請求進(jìn)行訪問的。(隨著下面的內(nèi)容深入體會JWT的核心)
二. JWT深度剖析
1. JWT的長相
下面的一段字符串就是JWT加密后的顯示格式,我們仔細(xì)看,中間通過兩個 “點(diǎn)” 將這段字符串分割成三部分了。
eyJ0eXAxIjoiMTIzNCIsImFsZzIiOiJhZG1pbiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU1MjI4Njc0Ni44Nzc0MDE4fQ.pEgdmFAy73walFonEm2zbxg46Oth3dlT02HR9iVzXa8上面一段很長的字符串到底是怎么來的呢?就需要了解JWT的構(gòu)成原理。
2. JWT的構(gòu)成
JWT由三部分組成,如下圖,分別是:Header頭部、Payload負(fù)載、Signature簽名。
?
(1). 頭部(Header)
通常包括兩部分,類型(如 “typ”:“JWT”)和加密算法(如“alg”:"HS256"),當(dāng)然你也可以添加其它自定義的一些參數(shù),然后對這個對象機(jī)型base64編碼,生成一段字符串,
如“eyJ0eXAxIjoiMTIzNCIsImFsZzIiOiJhZG1pbiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0”,我們可以對其進(jìn)行反編碼一下,看一下其廬山真面目。
?注:Base64是一種編碼,也就是說,它是可以被翻譯回原來的樣子來的。它并不是一種加密過程。
(2). 負(fù)載(Payload)
通常用來存放一些業(yè)務(wù)需要但不敏感的信息,比如:用戶編號(userId)、用戶賬號(userAccount)、權(quán)限等等,該部分也有一些默認(rèn)的聲明,如下圖,很多不常用。
- iss: jwt簽發(fā)者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大于簽發(fā)時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發(fā)時間
- jti: jwt的唯一身份標(biāo)識,主要用來作為一次性token,從而回避重放攻擊。
其中最常用的就是exp過期時間,要和1970年1月1日那個點(diǎn)進(jìn)行比對,用法如下,下面表示生成jwt字符串后20分鐘過期。
?
最后對該部分組裝成的對象進(jìn)行base64編碼,如:“eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU1MjI4Njc0Ni44Nzc0MDE4fQ”,我們可以對其進(jìn)行反編碼看一下廬山真面目,如下圖:
?
注:該部分也是可以解碼的,所以不要存儲敏感信息。
(3). 簽名(Signature)
這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進(jìn)行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分。
偽代碼如下:
1 var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); 2 var signature = HMACSHA256(encodedString, 'sercret密鑰')說明:密鑰存在服務(wù)器端,不要泄露,在不知道密鑰的情況下,是不能進(jìn)行解密的,jwt的簽發(fā)生成也是在服務(wù)器端的,secret就是用來進(jìn)行jwt的簽發(fā)和jwt的驗(yàn)證,所以,它就是你服務(wù)端的私鑰,在任何場景都不應(yīng)該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發(fā)jwt了。
特別說明:即使payload中的信息被篡改,服務(wù)器端通過signature就可以判斷出來是非法請求,即校驗(yàn)不能通過。
3. 代碼嘗鮮
需要通過Nuget裝JWT包,新版本的jwt建議.Net 版本4.6起。
1 [HttpGet]2 public string JiaM()3 {4 //設(shè)置過期時間(可以不設(shè)置,下面表示簽名后 20分鐘過期)5 double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;6 var payload = new Dictionary<string, object>7 {8 { "UserId", 123 },9 { "UserName", "admin" }, 10 {"exp",exp } //該參數(shù)也可以不寫 11 }; 12 var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//不要泄露 13 IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); 14 15 //注意這個是額外的參數(shù),默認(rèn)參數(shù)是 typ 和alg 16 var headers = new Dictionary<string, object> 17 { 18 { "typ1", "1234" }, 19 { "alg2", "admin" } 20 }; 21 22 IJsonSerializer serializer = new JsonNetSerializer(); 23 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 24 IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); 25 var token = encoder.Encode(headers, payload, secret); 26 return token; 27 } 28 29 [HttpGet] 30 public string JieM(string token) 31 { 32 var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; 33 try 34 { 35 IJsonSerializer serializer = new JsonNetSerializer(); 36 IDateTimeProvider provider = new UtcDateTimeProvider(); 37 IJwtValidator validator = new JwtValidator(serializer, provider); 38 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 39 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); 40 var json = decoder.Decode(token, secret, true); 41 return json; 42 } 43 catch (TokenExpiredException) 44 { 45 //過期了自動進(jìn)入這里 46 return "Token has expired"; 47 } 48 catch (SignatureVerificationException) 49 { 50 //校驗(yàn)未通過自動進(jìn)入這里 51 return "Token has invalid signature"; 52 } 53 catch (Exception) 54 { 55 //其它錯誤,自動進(jìn)入到這里 56 return "other error"; 57 }上述代碼方便通過PostMan進(jìn)行快速測試,注意解密的方法中的三個catch,token過期,會自動進(jìn)入到TokenExpiredException異常中,token驗(yàn)證不通過,會自動進(jìn)入SignatureVerificationException中。
?
三. JWT的使用流程
? 整體流程,大致如下圖:
?
1. 客戶端(前端或App端)通過一個Http請求把用戶名和密碼傳到登錄接口,建議采用Https的模式,避免信息被嗅探。
2. 服務(wù)器端校驗(yàn)登錄的接口驗(yàn)證用戶名和密碼通過后,把一些業(yè)務(wù)邏輯需要的信息如:userId、userAccount放到Payload中,進(jìn)而生成一個xxx.yyy.zzz形式的JWT字符串返回給客戶端。
3. 客戶端獲取到JWT的字符串,可以存放到LocalStorage中,注意退出的登錄的時候刪除該值。
4. 登錄成功,每次請求其它接口的時候都在表頭帶著該jwt字符串,建議放入HTTP Header中的Authorization位。(解決XSS和XSRF問題)? 或者自己命名比如:“auth”,進(jìn)行該字符串的傳遞。
5. 服務(wù)器端要寫一個過濾器,在該過濾器中進(jìn)行校驗(yàn)jwt的有效性(簽名是否正確、是否過期),驗(yàn)證通過進(jìn)行接口的業(yè)務(wù)邏輯,驗(yàn)證不通過,返回給客戶端。
這里要解決兩個問題?
(1). 在WebApi的過濾器中,如果校驗(yàn)通過了,如何將解密后的值傳遞到action中。(解密兩次就有點(diǎn)坑了)
(2). 在WebApi的過濾器中,如果校驗(yàn)不通過,如何返回給客戶端,然后客戶端針對這種情況,又該如何接受呢。
(實(shí)戰(zhàn)中揭曉)。
?
四. 項目實(shí)戰(zhàn)
一. 整體目標(biāo):
通過一個登陸接口和一個獲取信息的接口模擬JWT的整套驗(yàn)證邏輯。
二. 詳細(xì)步驟
1. 封裝JWT加密和解密的方法。
需要通過Nuget安裝JWT的程序集,JWT的最新版本建議使用.Net 4.6 起。
?JWTHelp
2. 模擬登陸接口
在登錄接口中,模擬數(shù)據(jù)庫校驗(yàn),即賬號和密碼為admin和12345,即校驗(yàn)通過,然后把賬號和userId(實(shí)際應(yīng)該到數(shù)據(jù)庫中查),這里也可以設(shè)置一下過期時間,比如20分鐘,一同存放到PayLoad中,然后生成JWT字符串,返回給客戶端。
/// <summary>/// 模擬登陸/// </summary>/// <param name="userAccount"></param>/// <param name="pwd"></param>/// <returns></returns>[HttpGet]public string Login1(string userAccount, string pwd){try{//這里模擬數(shù)據(jù)操作,只要是admin和123456就驗(yàn)證通過if (userAccount == "admin" && pwd == "123456"){//1. 進(jìn)行業(yè)務(wù)處理(這里模擬獲取userId)string userId = "0806";//過期時間(可以不設(shè)置,下面表示簽名后 20分鐘過期)double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;//進(jìn)行組裝var payload = new Dictionary<string, object>{{"userId", userId },{"userAccount", userAccount },{"exp",exp }};//2. 進(jìn)行JWT簽名var token = JWTHelp.JWTJiaM(payload);var result = new { result = "ok", token = token };return JsonConvert.SerializeObject(result);}else{var result = new { result = "error", token = "" };return JsonConvert.SerializeObject(result);}}catch (Exception){var result = new { result = "error", token = "" };return JsonConvert.SerializeObject(result);}}3. 客戶端調(diào)用登錄接口
這里只是單純?yōu)榱藴y試,使用的get請求,實(shí)際項目中建議post請求,且配置Https,請求成功后,把jwt字符串存放到localStorage中。
1 //1.登錄2 $('#j_jwtLogin').on('click', function () {3 $.get("/api/Seventh/Login1", { userAccount: "admin", pwd: "123456" }, function (data) {4 var jsonData = JSON.parse(data);5 if (jsonData.result == "ok") {6 console.log(jsonData.token);7 //存放到本地緩存中8 window.localStorage.setItem("token", jsonData.token);9 alert("登錄成功,ticket=" + jsonData.token); 10 } else { 11 alert("登錄失敗"); 12 } 13 }); 14 });運(yùn)行結(jié)果:
4. 服務(wù)器端過濾器
?代碼中分享了兩種獲取header中信息的方式,獲取到“auth”后,進(jìn)行校驗(yàn),校驗(yàn)不通過的話,通過狀態(tài)碼401返回給客戶端,校驗(yàn)通過的話,則使用?actionContext.RequestContext.RouteData.Values.Add("auth", result); 進(jìn)行解密值的存儲,方便后續(xù)action的直接獲取。
1 /// <summary>2 /// 驗(yàn)證JWT算法的過濾器3 /// </summary>4 public class JWTCheck : AuthorizeAttribute5 {6 public override void OnAuthorization(HttpActionContext actionContext)7 {8 //獲取表頭Header中值的幾種方式9 //方式一: 10 //{ 11 // var authHeader2 = from t in actionContext.Request.Headers 12 // where t.Key == "auth" 13 // select t.Value.FirstOrDefault(); 14 // var token2 = authHeader2.FirstOrDefault(); 15 //} 16 17 //方式二: 18 IEnumerable<string> auths; 19 if (!actionContext.Request.Headers.TryGetValues("auth", out auths)) 20 { 21 //HttpContext.Current.Response.Write("報文頭中的auth為空"); 22 //返回狀態(tài)碼驗(yàn)證未通過,并返回原因(前端進(jìn)行401狀態(tài)碼的捕獲),注意:這句話并不能階段該過濾器,還會繼續(xù)往下走,要借助if-else 23 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("報文頭中的auth為空")); 24 } 25 else 26 { 27 var token = auths.FirstOrDefault(); 28 if (token != null) 29 { 30 if (!string.IsNullOrEmpty(token)) 31 { 32 var result = JWTHelp.JWTJieM(token); 33 if (result == "expired") 34 { 35 //返回狀態(tài)碼驗(yàn)證未通過,并返回原因(前端進(jìn)行401狀態(tài)碼的捕獲) 36 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("expired")); 37 } 38 else if (result == "invalid") 39 { 40 //返回狀態(tài)碼驗(yàn)證未通過,并返回原因(前端進(jìn)行401狀態(tài)碼的捕獲) 41 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("invalid")); 42 } 43 else if (result == "error") 44 { 45 //返回狀態(tài)碼驗(yàn)證未通過,并返回原因(前端進(jìn)行401狀態(tài)碼的捕獲) 46 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("error")); 47 } 48 else 49 { 50 //表示校驗(yàn)通過,用于向控制器中傳值 51 actionContext.RequestContext.RouteData.Values.Add("auth", result); 52 } 53 } 54 } 55 else 56 { 57 //返回狀態(tài)碼驗(yàn)證未通過,并返回原因(前端進(jìn)行401狀態(tài)碼的捕獲) 58 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("token 空")); 59 } 60 } 61 62 } 63 }5.服務(wù)器端獲取信息的的方法
? 將上說過濾器以特性的形式作用在該方法中,然后通過 RequestContext.RouteData.Values["auth"] 獲取到解密后的值,進(jìn)而進(jìn)行其它業(yè)務(wù)處理。
1 /// <summary>2 /// 加密后的獲取信息3 /// </summary>4 /// <returns></returns>5 [JWTCheck]6 [HttpGet]7 public string GetInfor()8 {9 var userData = JsonConvert.DeserializeObject<userData>(RequestContext.RouteData.Values["auth"].ToString()); ; 10 if (userData == null) 11 { 12 var result = new { Message = "error", data = "" }; 13 return JsonConvert.SerializeObject(result); 14 } 15 else 16 { 17 var data = new { userId = userData.userId, userAccount = userData.userAccount }; 18 var result = new { Message = "ok", data =data }; 19 return JsonConvert.SerializeObject(result); 20 } 21 }6. 客戶端調(diào)用獲取信息的方法?
前端獲取到localStorage中token值,采用自定義header的方式以“auth”進(jìn)行傳遞調(diào)用服務(wù)器端的方法,由于服務(wù)器的驗(yàn)證token不正確的時候,是以狀態(tài)碼的形式返回,所以這里要采用error方法,通過xhr.status==401進(jìn)行判斷,凡是進(jìn)入到這個401中,均是token驗(yàn)證沒有通過,具體是什么原因,可以通過xhr.responseText獲取詳細(xì)的值進(jìn)行判斷。
1 //2.獲取信息2 $('#j_jwtGetInfor').on('click', function () {3 //從本地緩存中讀取token值4 var token = window.localStorage.getItem("token");5 $.ajax({6 url: "/api/Seventh/GetInfor",7 type: "Get",8 data: {},9 datatype: "json", 10 //設(shè)置header的方式1 11 headers: { "auth": token}, 12 //設(shè)置header的方式2 13 //beforeSend: function (xhr) { 14 // xhr.setRequestHeader("auth", token) 15 //}, 16 success: function (data) { 17 console.log(data); 18 var jsonData = JSON.parse(data); 19 if (jsonData.Message == "ok") { 20 var myData = jsonData.data; 21 console.log("獲取成功"); 22 console.log(myData.userId); 23 console.log(myData.userAccount); 24 } else { 25 console.log("獲取失敗"); 26 } 27 }, 28 //當(dāng)安全校驗(yàn)未通過的時候進(jìn)入這里 29 error: function (xhr) { 30 if (xhr.status == 401) { 31 console.log(xhr.responseText); 32 var jsonData = JSON.parse(xhr.responseText); 33 console.log("授權(quán)失敗,原因?yàn)?#xff1a;" + jsonData.Message); 34 } 35 } 36 }); 37 });運(yùn)行結(jié)果:
?
?
其他的如token過期只需要改一下電腦時間即可以測試,token不正確改一下獲取到的jwt字符串可以測試,這里不再進(jìn)行 了。
總結(jié)
以上是生活随笔為你收集整理的第九节:JWT简介和以JS+WebApi为例基于JWT的安全校验的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为什么TypedReference在幕后
- 下一篇: 第一节 从面向对象的角度重新认识JS世界