1.什麼是JWT Token
JWT(Json Web Tokens) 是一個開放標準(RFC 7519),它定義了一種簡潔,自包含,JSON 對象形式的安全傳遞信息的方法。JWT經常使用在 Web 應用或者移動應用上,Token是令牌的意思,表示只有拿着令牌才具備一些權限。JWT的聲明(Claim)通常被用來在身份提供者和服務提供者間傳遞身份驗證信息,也能夠增長一些額外的其它業務邏輯所必須的聲明信息。git
JWT的使用場景
一次性驗證github
好比用戶註冊後須要發一封郵件讓其激活帳戶,一般郵件中須要有一個連接,這個連接須要具有如下的特性:可以標識用戶,該連接具備時效性(一般只容許幾小時以內激活),不能被篡改以激活其餘可能的帳戶…這種場景就和 jwt 的特性很是貼近,jwt 的 payload 中固定的參數:iss 簽發者和 exp 過時時間正是爲其作準備的。redis
restful api 的無狀態認證算法
使用 jwt 來作 restful api 的身份認證也是值得推崇的一種使用方案。客戶端和服務端共享 secret;過時時間由服務端校驗,客戶端定時刷新json
2.JWT的組成
使用JWT token認證前咱們先了解下JWT的組成部分。JWT通過加密處理與校驗處理的字符串,形式 A.B.Capi
A由JWT頭部信息header加密獲得 B由JWT用到的身份驗證信息json數據加密獲得 C由A和B加密獲得,是校驗部分
怎麼計算A?
header格式爲: { "typ": "JWT", "alg": "HS256" }
它就是一個json串,兩個字段是必須的,不能多也不能少。alg
字段指定了生成C的算法,默認值是HS256,將header用base64加密,獲得A。
補充:Base64是一種表示二進制數據的方法。因爲2的6次方等於64,因此每6個比特爲一個單元,對應某一個可打印字符。三個字節有24個比特,對應於4個Base64單元,即3個字節須要用4個Base64可打印字符來表示。瀏覽器
怎樣計算B?
根據JWT claim set[用base64]加密獲得的。claim set是一個json數據,是代表用戶身份的數據,可自行指定字段很靈活,也有固定字段表示特定含義(但不必定要包含特定字段,只是推薦)。
一些字段的含義:緩存
{
"iss" :"http://example.org", //非必須。issuer 請求實體,能夠是發起請求的用戶的信息,也但是jwt的簽發者。 "iat" : 1356999524, //非必須。issued at。 token建立時間,unix時間戳格式 "exp" :"1548333419", //非必須。expire 指定token的生命週期。unix時間戳格式 "aud" : "http://example.com", //非必須。接收該JWT的一方。 "sub" : "jrocket@example.com", //非必須。該JWT所面向的用戶 "nbf" : 1357000000, //非必須。not before。若是當前時間在nbf裏的時間以前,則Token不被接受;通常都會留一些餘地,好比幾分鐘。 "jti" :'222we', //非必須。JWT ID。針對當前token的惟一標識 "GivenName" : "Jonny", // 自定義字段 "Surname" : "Rocket", // 自定義字段 "Email" : "jrocket@example.com", // 自定義字段 "Role" : ["Manager", "Project Administrator"] // 自定義字段
}
自定義字段的key是一個string,value是一個json數據。將claim set經過Base64加密後獲得B
,學名payload(載荷)
安全
怎樣計算C?
將A.B
使用HS256加密(實際上是用header中指定的算法),固然加密過程當中還須要密鑰(secret,自行指定的一個字符串)。加密獲得C
,學名signature
,其實就是一個字符串。
3.JWT的工做過程
借鑑於:https://www.cnblogs.com/lonelyxmas/p/8024006.html
下面咱們從一個實例來看如何運用JWT機制實現認證:
1.登陸
- 第一次認證:第一次登陸,用戶從瀏覽器輸入用戶名/密碼,提交後到服務器的登陸處理的Action層(Login Action);
- Login Action調用認證服務進行用戶名密碼認證,若是認證經過,Login Action層調用用戶信息服務獲取用戶信息(包括完整的用戶信息及對應權限信息);
- 返回用戶信息後,Login Action從配置文件中獲取Token簽名生成的祕鑰信息,進行Token的生成;
- 生成Token的過程當中能夠調用第三方的JWT Lib生成簽名後的JWT數據;
- 完成JWT數據簽名後,將其設置到COOKIE對象中,並重定向到首頁,完成登陸過程;
2.請求認證
基於Token的認證機制會在每一次請求中都帶上完成簽名的Token信息,這個Token信息可能在Cookie中,也可能在HTTP的Authorization頭中;
- 客戶端(APP客戶端或瀏覽器)經過GET或POST請求訪問資源(頁面或調用API);
- 認證服務做爲一個Middleware HOOK 對請求進行攔截,首先在cookie中查找Token信息,若是沒有找到,則在HTTP Authorization Head中查找;
- 若是找到Token信息,則根據配置文件中的簽名加密祕鑰,調用JWT Lib對Token信息進行解密和解碼;
- 完成解碼並驗證簽名經過後,對Token中的exp、nbf、aud等信息進行驗證;
- 所有經過後,根據獲取的用戶的角色權限信息,進行對請求的資源的權限邏輯判斷;
- 若是權限邏輯判斷經過則經過驗證,開始執行功能代碼;不然則返回HTTP 401;
3.JWT.Net的使用
使用JWT.Net前要首先經過Nuge(git地址:https://github.com/jwt-dotnet/jwt)t獲取JWT.Net包,以下:
添加了JWT.Net包後就可使用JWT了,這裏封裝了一個簡單的JWTHelper,代碼以下:
public class JWTHelper { static IJwtAlgorithm algorithm = new HMACSHA256Algorithm();//HMACSHA256加密 static IJsonSerializer serializer = new JsonNetSerializer();//序列化和反序列 static IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();//Base64編解碼 static IDateTimeProvider provider = new UtcDateTimeProvider();//UTC時間獲取 #region /////生成JWT public static string GetJWT(string secret, Dictionary<string, object> payload) { IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); return encoder.Encode(payload, secret); } #endregion #region /////驗證JWT public static bool ValidateJwt(string secret, string token, out string payload,out string message) { bool isValidted = false; payload = ""; try { IJwtValidator validator = new JwtValidator(serializer, provider);//用於驗證JWT的類 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);//用於解析JWT的類 payload = decoder.Decode(token, secret, verify: true);//verify:true表示解析JWT時進行驗證,該方法會自動調用validator的Validate()方法,不知足驗證會拋出異常,所以咱們不用寫驗證的方法 isValidted = true; message = "驗證成功"; } catch (TokenExpiredException)//若是當前時間大於負載中的過時時間(負荷中的exp),引起Token過時異常 { message = "Token已通過期了!"; } catch (SignatureVerificationException)//若是簽名不匹配,引起簽名驗證異常 { message = "Token簽名不正確!"; } return isValidted; } #endregion }
咱們在一個控制檯程序中簡單展現一下JWT.Net的用法,代碼以下:
static void Main(string[] args) { //服務端的祕鑰,通常放在配置文件中 const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; //負荷(payload) var payload = new Dictionary<string, object> { { "claim1", 0 }, { "testStr", "hello" }, {"testObj" ,new { name="111"} }, { "exp", DateTimeOffset.UtcNow.AddSeconds(2).ToUnixTimeSeconds() } }; Console.WriteLine("生成JWT--------------------------------------------------------------"); Console.WriteLine(); string token = JWTHelper.GetJWT(secret, payload); Console.WriteLine($"生成的JWT是:{token}"); Console.WriteLine(); Console.WriteLine(); Console.WriteLine(); Console.WriteLine("校驗JWT--------------------------------------------------------------"); Console.WriteLine(); string message;//解析的消息 string curPayload;//解析獲取的負載 if (JWTHelper.ValidateJwt(secret,token,out curPayload,out message)) { Console.WriteLine($"解析獲取的負載是:{curPayload}"); } Console.WriteLine(message); } }
運行結果如圖所示:
4.一些須要注意的問題
這些問題摘自https://blog.csdn.net/qq_28165595/article/details/80214994
1.jwt token泄露了怎麼辦?
前面的文章下有很多人留言提到這個問題,我則認爲這不是問題。傳統的 session+cookie 方案,若是泄露了 sessionId,別人一樣能夠盜用你的身份。揚湯止沸不如釜底抽薪,不妨來追根溯源一下,什麼場景會致使你的 jwt 泄露。
遵循以下的實踐能夠儘量保護你的 jwt 不被泄露:使用 https 加密你的應用,返回 jwt 給客戶端時設置 httpOnly=true 而且使用 cookie 而不是 LocalStorage 存儲 jwt,這樣能夠防止 XSS 攻擊和 CSRF 攻擊
2. secret如何設計
JWT惟一存儲在服務端的只有一個 secret,我的認爲這個 secret 應該設計成和用戶相關的屬性,而不是一個全部用戶公用的統一值。這樣能夠有效的避免一些註銷和修改密碼時遇到的窘境。
註銷和修改密碼
傳統的 session+cookie 方案用戶點擊註銷,服務端清空 session 便可,由於狀態保存在服務端。但 jwt 的方案就比較難辦了,由於 jwt 是無狀態的,服務端經過計算來校驗有效性。沒有存儲起來,因此即便客戶端刪除了 jwt,可是該 jwt 仍是在有效期內,只不過處於一個遊離狀態。分析下痛點:註銷變得複雜的緣由在於 jwt 的無狀態。我提供幾個方案,視具體的業務來決定能不能接受。
1. 僅僅清空客戶端的 cookie,這樣用戶訪問時就不會攜帶 jwt,服務端就認爲用戶須要從新登陸。這是一個典型的假註銷,對於用戶表現出退出的行爲,實際上這個時候攜帶對應的 jwt 依舊能夠訪問系統。
2. 清空或修改服務端的用戶對應的 secret,這樣在用戶註銷後,jwt 自己不變,可是因爲 secret 不存在或改變,則沒法完成校驗。這也是爲何將 secret 設計成和用戶相關的緣由。
3. 藉助第三方存儲本身管理 jwt 的狀態,能夠以 jwt 爲 key,實現去 redis 一類的緩存中間件中去校驗存在性。方案設計並不難,可是引入 redis 以後,就把無狀態的 jwt 硬生生變成了有狀態了,違背了 jwt 的初衷。實際上這個方案和 session 都差很少了。
修改密碼則略微有些不一樣,假設號被到了,修改密碼(是用戶密碼,不是 jwt 的 secret)以後,盜號者在原 jwt 有效期以內依舊能夠繼續訪問系統,因此僅僅清空 cookie 天然是不夠的,這時,須要強制性的修改 secret。在個人實踐中就是這樣作的。
3.續簽問題
續約問題能夠說是我抵制使用 jwt 來代替傳統 session 的最大緣由,由於 jwt 的設計中我就沒有發現它將續簽認爲是自身的一個特性。傳統的 cookie 續簽方案通常都是框架自帶的,session 有效期 30 分鐘,30 分鐘內若是有訪問,session 有效期被刷新至 30 分鐘。而 jwt 自己的 payload 之中也有一個 exp 過時時間參數,來表明一個 jwt 的時效性,而 jwt 想延期這個 exp 就有點身不禁己了,由於 payload 是參與簽名的,一旦過時時間被修改,整個 jwt 串就變了,jwt 的特性自然不支持續簽!
若是你必定要使用 jwt 作會話管理(payload 中存儲會話信息),也不是沒有解決方案,但我的認爲都不是很使人滿意
1.每次請求刷新JWT
JWT修改 payload 中的 exp 後整個 jwt 串就會發生改變,那…就讓它變好了,每次請求都返回一個新的 jwt 給客戶端。太暴力了,不用我贅述這樣作是多麼的不優雅,以及帶來的性能問題。但,至少這是最簡單的解決方案。
2.只要快要過時的時候刷新JWT
一個上述方案的改造點是,只在最後的幾分鐘返回給客戶端一個新的 jwt。這樣作,觸發刷新 jwt 基本就要看運氣了,若是用戶恰巧在最後幾分鐘訪問了服務器,觸發了刷新,萬事大吉;若是用戶連續操做了 27 分鐘,只有最後的 3 分鐘沒有操做,致使未刷新 jwt,無疑會令用戶抓狂。
3.完善 refreshToken
借鑑 oauth2 的設計,返回給客戶端一個 refreshToken,容許客戶端主動刷新 jwt。通常而言,jwt 的過時時間能夠設置爲數小時,而 refreshToken 的過時時間設置爲數天。我認爲該方案並可行性是存在的,可是爲了解決 jwt 的續簽把整個流程改變了,爲何不考慮下 oauth2 的 password 模式和 client 模式呢?
4.使用 redis 記錄獨立的過時時間
爲了解決續簽問題,在 redis 中單獨給每一個 jwt 設置了過時時間,每次訪問時刷新 jwt 的過時時間,若 jwt 不存在於 redis 中則認爲過時。
一樣改變了 jwt 的流程,不過嘛,世間安得兩全法。我只能奉勸各位還未使用 jwt 作會話管理的朋友,儘可能仍是選用傳統的 session+cookie 方案,有不少成熟的分佈式 session 框架和安全框架供你開箱即用。
參考文章:
1.https://blog.csdn.net/mn_kw/article/details/80522565
2.http://www.javashuo.com/article/p-cgntecus-dx.html
3.https://blog.csdn.net/qq_28165595/article/details/80214994