一. 簡介前端
1. 背景web
傳統的基於Session的校驗存在諸多問題,好比:Session過時、服務器開銷過大、不能分佈式部署、不適合先後端分離的項目。 傳統的基於Token的校驗須要存儲Key-Value信息,存在Session或數據庫中都有弊端,若是按照必定規律採用對稱加密算法生成token,雖然能解決上面問題,可是一旦對稱加密算法泄露,很容被反編譯;因此在此基礎上繼續升級,利用userId生成Token,只要保存好祕鑰便可,從而引出JWT。ajax
2. 什麼是JWT算法
Json web token 簡稱:JWT, 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準。該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。數據庫
下面就是一段JWT字符串(後面詳細分析)json
1 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U
3. JWT的優勢後端
(1). JWT是無狀態的,不須要服務器端保存會話信息,減輕服務器端的讀取壓力(存儲在客戶端上),同時易於擴展、易於分佈式部署。api
(2). JWT能夠跨語言支持。緩存
(3). 便於傳輸,jwt的構成很簡單,字節佔用空間少,因此是很是便於傳輸的。安全
(4). 自身構成有payload部分,能夠存儲一下業務邏輯相關的非敏感信息。
特別聲明:JWT最大的優點是無狀態的,相對傳統的Session驗證能減輕服務器端的存儲壓力,安全性更高,但也不是絕對的,好比針對同一個接口,JWT字符串被截取後,且在有效期內,在不篡改JWT字符串的狀況下,也是能夠模擬請求進行訪問的。(隨着下面的內容深刻體會JWT的核心)
二. JWT深度剖析
1. JWT的長相
下面的一段字符串就是JWT加密後的顯示格式,咱們仔細看,中間經過兩個 「點」 將這段字符串分割成三部分了。
eyJ0eXAxIjoiMTIzNCIsImFsZzIiOiJhZG1pbiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU1MjI4Njc0Ni44Nzc0MDE4fQ.pEgdmFAy73walFonEm2zbxg46Oth3dlT02HR9iVzXa8
上面一段很長的字符串究竟是怎麼來的呢?就須要瞭解JWT的構成原理。
2. JWT的構成
JWT由三部分組成,以下圖,分別是:Header頭部、Payload負載、Signature簽名。
(1). 頭部(Header)
一般包括兩部分,類型(如 「typ」:「JWT」)和加密算法(如「alg」:"HS256"),固然你也能夠添加其它自定義的一些參數,而後對這個對象機型base64編碼,生成一段字符串,
如「eyJ0eXAxIjoiMTIzNCIsImFsZzIiOiJhZG1pbiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0」,咱們能夠對其進行反編碼一下,看一下其廬山真面目。
注:Base64是一種編碼,也就是說,它是能夠被翻譯回原來的樣子來的。它並非一種加密過程。
(2). 負載(Payload)
一般用來存放一些業務須要但不敏感的信息,好比:用戶編號(userId)、用戶帳號(userAccount)、權限等等,該部分也有一些默認的聲明,以下圖,不少不經常使用。
最後對該部分組裝成的對象進行base64編碼,如:「eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU1MjI4Njc0Ni44Nzc0MDE4fQ」,咱們能夠對其進行反編碼看一下廬山真面目,以下圖:
注:該部分也是能夠解碼的,因此不要存儲敏感信息。
(3). 簽名(Signature)
這個部分須要base64加密後的header和base64加密後的payload使用.
鏈接組成的字符串,而後經過header中聲明的加密方式進行加鹽secret
組合加密,而後就構成了jwt的第三部分。
僞代碼以下:
1 var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); 2 var signature = HMACSHA256(encodedString, 'sercret密鑰')
說明:密鑰存在服務器端,不要泄露,在不知道密鑰的狀況下,是不能進行解密的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,因此,它就是你服務端的私鑰,在任何場景都不該該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是能夠自我簽發jwt了。
特別說明:即便payload中的信息被篡改,服務器端經過signature就能夠判斷出來是非法請求,即校驗不能經過。
3. 代碼嚐鮮
須要經過Nuget裝JWT包,新版本的jwt建議.Net 版本4.6起。
1 [HttpGet] 2 public string JiaM() 3 { 4 //設置過時時間(能夠不設置,下面表示簽名後 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 } //該參數也能夠不寫 11 }; 12 var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//不要泄露 13 IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); 14 15 //注意這個是額外的參數,默認參數是 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 //過時了自動進入這裏 46 return "Token has expired"; 47 } 48 catch (SignatureVerificationException) 49 { 50 //校驗未經過自動進入這裏 51 return "Token has invalid signature"; 52 } 53 catch (Exception) 54 { 55 //其它錯誤,自動進入到這裏 56 return "other error"; 57 }
上述代碼方便經過PostMan進行快速測試,注意解密的方法中的三個catch,token過時,會自動進入到TokenExpiredException異常中,token驗證不經過,會自動進入SignatureVerificationException中。
三. JWT的使用流程
總體流程,大體以下圖:
1. 客戶端(前端或App端)經過一個Http請求把用戶名和密碼傳到登陸接口,建議採用Https的模式,避免信息被嗅探。
2. 服務器端校驗登陸的接口驗證用戶名和密碼經過後,把一些業務邏輯須要的信息如:userId、userAccount放到Payload中,進而生成一個xxx.yyy.zzz形式的JWT字符串返回給客戶端。
3. 客戶端獲取到JWT的字符串,能夠存放到LocalStorage中,注意退出的登陸的時候刪除該值。
4. 登陸成功,每次請求其它接口的時候都在表頭帶着該jwt字符串,建議放入HTTP Header中的Authorization位。(解決XSS和XSRF問題) 或者本身命名好比:「auth」,進行該字符串的傳遞。
5. 服務器端要寫一個過濾器,在該過濾器中進行校驗jwt的有效性(簽名是否正確、是否過時),驗證經過進行接口的業務邏輯,驗證不經過,返回給客戶端。
這裏要解決兩個問題?
(1). 在WebApi的過濾器中,若是校驗經過了,如何將解密後的值傳遞到action中。(解密兩次就有點坑了)
(2). 在WebApi的過濾器中,若是校驗不經過,如何返回給客戶端,而後客戶端針對這種狀況,又該如何接受呢。
(實戰中揭曉)。
四. 項目實戰
一. 總體目標:
經過一個登錄接口和一個獲取信息的接口模擬JWT的整套驗證邏輯。
二. 詳細步驟
1. 封裝JWT加密和解密的方法。
須要經過Nuget安裝JWT的程序集,JWT的最新版本建議使用.Net 4.6 起。
1 public class JWTHelp 2 { 3 4 /// <summary> 5 /// JWT加密算法 6 /// </summary> 7 /// <param name="payload">負荷部分,存儲使用的信息</param> 8 /// <returns></returns> 9 public static string JWTJiaM(IDictionary<string, object> payload) 10 { 11 //密鑰保存好,不要泄露 12 var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; 13 IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); 14 IJsonSerializer serializer = new JsonNetSerializer(); 15 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 16 IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); 17 var token = encoder.Encode(payload, secret); 18 return token; 19 } 20 21 /// <summary> 22 /// JWT解密算法 23 /// </summary> 24 /// <param name="token"></param> 25 /// <returns></returns> 26 public static string JWTJieM(string token) 27 { 28 //密鑰保存好,不要泄露 29 var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; 30 try 31 { 32 IJsonSerializer serializer = new JsonNetSerializer(); 33 IDateTimeProvider provider = new UtcDateTimeProvider(); 34 IJwtValidator validator = new JwtValidator(serializer, provider); 35 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); 36 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); 37 var json = decoder.Decode(token, secret, true); 38 //校驗經過,返回解密後的字符串 39 return json; 40 } 41 catch (TokenExpiredException) 42 { 43 //表示過時 44 return "expired"; 45 } 46 catch (SignatureVerificationException) 47 { 48 //表示驗證不經過 49 return "invalid"; 50 } 51 catch (Exception ex) 52 { 53 return "error"; 54 } 55 } 56 57 58 }
2. 模擬登錄接口
在登陸接口中,模擬數據庫校驗,即帳號和密碼爲admin和12345,即校驗經過,而後把帳號和userId(實際應該到數據庫中查),這裏也能夠設置一下過時時間,好比20分鐘,一同存放到PayLoad中,而後生成JWT字符串,返回給客戶端。
/// <summary> /// 模擬登錄 /// </summary> /// <param name="userAccount"></param> /// <param name="pwd"></param> /// <returns></returns> [HttpGet] public string Login1(string userAccount, string pwd) { try { //這裏模擬數據操做,只要是admin和123456就驗證經過 if (userAccount == "admin" && pwd == "123456") { //1. 進行業務處理(這裏模擬獲取userId) string userId = "0806"; //過時時間(能夠不設置,下面表示簽名後 20分鐘過時) double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds; //進行組裝 var payload = new Dictionary<string, object> { {"userId", userId }, {"userAccount", userAccount }, {"exp",exp } }; //2. 進行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. 客戶端調用登陸接口
這裏只是單純爲了測試,使用的get請求,實際項目中建議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 });
運行結果:
4. 服務器端過濾器
代碼中分享了兩種獲取header中信息的方式,獲取到「auth」後,進行校驗,校驗不經過的話,經過狀態碼401返回給客戶端,校驗經過的話,則使用 actionContext.RequestContext.RouteData.Values.Add("auth", result); 進行解密值的存儲,方便後續action的直接獲取。
1 /// <summary> 2 /// 驗證JWT算法的過濾器 3 /// </summary> 4 public class JWTCheck : AuthorizeAttribute 5 { 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 //返回狀態碼驗證未經過,並返回緣由(前端進行401狀態碼的捕獲),注意:這句話並不能截斷該過濾器,還會繼續往下走,要藉助if-else,若是想直接截斷,須要加 return; 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 //返回狀態碼驗證未經過,並返回緣由(前端進行401狀態碼的捕獲) 36 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("expired")); 37 } 38 else if (result == "invalid") 39 { 40 //返回狀態碼驗證未經過,並返回緣由(前端進行401狀態碼的捕獲) 41 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("invalid")); 42 } 43 else if (result == "error") 44 { 45 //返回狀態碼驗證未經過,並返回緣由(前端進行401狀態碼的捕獲) 46 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("error")); 47 } 48 else 49 { 50 //表示校驗經過,用於向控制器中傳值 51 actionContext.RequestContext.RouteData.Values.Add("auth", result); 52 } 53 } 54 } 55 else 56 { 57 //返回狀態碼驗證未經過,並返回緣由(前端進行401狀態碼的捕獲) 58 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("token 空")); 59 } 60 } 61 62 } 63 }
5.服務器端獲取信息的的方法
將上說過濾器以特性的形式做用在該方法中,而後經過 RequestContext.RouteData.Values["auth"] 獲取到解密後的值,進而進行其它業務處理。
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. 客戶端調用獲取信息的方法
前端獲取到localStorage中token值,採用自定義header的方式以「auth」進行傳遞調用服務器端的方法,因爲服務器的驗證token不正確的時候,是以狀態碼的形式返回,因此這裏要採用error方法,經過xhr.status==401進行判斷,凡是進入到這個401中,均是token驗證沒有經過,具體是什麼緣由,能夠經過xhr.responseText獲取詳細的值進行判斷。
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 //設置header的方式1 11 headers: { "auth": token}, 12 //設置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 //當安全校驗未經過的時候進入這裏 29 error: function (xhr) { 30 if (xhr.status == 401) { 31 console.log(xhr.responseText); 32 var jsonData = JSON.parse(xhr.responseText); 33 console.log("受權失敗,緣由爲:" + jsonData.Message); 34 } 35 } 36 }); 37 });
運行結果:
其餘的如token過時只須要改一下電腦時間便可以測試,token不正確改一下獲取到的jwt字符串能夠測試,這裏再也不進行 了。
!