淺析 JWT
JSON Web Token,簡稱 JWT,讀音是 [dʒɒt]( jot 的發音),是一種當下比較流行的「跨域認證解決方案」。 注意它是一套 RFC 規範,相關的還有 JWE/JWS/JWK/JOSE。 它有很多優點,也有侷限性,但我們可以配合其他方案做出適合自己業務的一套方案。 本篇是對 JWT 做一個簡單的介紹和簡單實踐總結。
JSON Web Token (JWT) is a compact claims representation format intended for space constrained environments such as HTTP Authorization headers and URI query parameters.
JWT的組成
JWT 由三部分組成:頭部、資料體、簽名/加密。
這三部分以 . (英文句號)連線,注意這三部分順序是固定的,即 header.payload.signature ,如下示例:
1. 頭部 The Header
這部分用來描述 JWT 的元資料,比如該 JWT 所使用的簽名/加密演算法、媒體型別等。
這部分原始資料是一個JSON物件,經過Base64Url編碼方式進行編碼後得到最終的字串。其中只有一個屬性是必要的: alg ——加密/簽名演算法,預設值為 HS256 。
最簡單的頭部可以表示成這樣:
其他 可選 屬性:
- typ ,描述 JWT 的媒體型別,該屬性的值只能是 JWT ,它的作用是與其他 JOSE Header 混合時表明自己身份的一個引數(很少用到)。
- cty ,描述 JWT 的內容型別。只有當需要一個 Nested JWT 時,才需要該屬性,且值必須是 JWT 。
- kid ,KeyID,用於提示是哪個金鑰參與加密。
Base64url 編碼是 Base64 的一種針對 URL 的特定變種。因為 = 、+、/ 這個三個字元在 URL 中是有特定含義的,所以 Base64url 分別將 = 直接忽略,+ 替換成 -,/ 替換成 _
2. 資料體 The Payload
這部分用來描述JWT的內容資料,即存放些什麼。
原始資料仍是一個 JSON 物件,經過 Base64url 編碼方式進行編碼後得到最終的 Payload。這裡的資料預設是不加密的,所以不應存放重要資料(當然你可以考慮使用巢狀型 JWT)。官方內建了七個屬性, 大小寫敏感 ,且都是可選屬性,如下:
- iss (Issuer) 簽發人,即簽發該 Token 的主體
- sub (Subject) 主題,即描述該 Token 的用途
- aud (Audience) 作用域,即描述這個 Token 是給誰用的,多個的情況下該屬性值為一個字串陣列,單個則為一個字串
- exp (Expiration Time) 過期時間,即描述該 Token 在何時失效
- nbf (Not Before) 生效時間,即描述該 Token 在何時生效
- iat (Issued At) 簽發時間,即描述該 Token 在何時被簽發的
- jti (JWT ID) 唯一標識
除了這幾個內建屬性,我們也可以自定義其他屬性,自由度非常大。
這裡對 aud 做一個說明,有如下 Payload:
那麼如果我拿這個 JWT 去 http://www.c.com 獲取有訪問許可權的資源,就會被拒絕掉,因為 aud 屬性明確了這個 Token 是無權訪問 www.c.com 的,有同學會說這部分反正不加密,那我本地把 www.c.com 加入進去不就完事了。別急,下面這部分看完先。
3. 簽名/加密 The signature/encryption data
這部分是相對比較複雜的,因為 JWT 必須符合 JWS/JWE 這兩個規範之一,所以針對這部分的資料如何得來就有兩種方式,我們先來看一個簡單的例子,有如下 JWT:
對前兩部分用 Base64url 解碼後能得出相應原始資料,
Header 部分:
Payload 部分:
根據 Header 部分的 alg 屬性我們可以知道該 JWT 符合 JWS 中的規範,且簽名演算法是 HS256 也就是 HMAC SHA-256 演算法,那麼我們就可以根據如下公式計算最後的簽名部分:
其中的金鑰是保證簽名安全性的關鍵,所以必須儲存好,在本例中金鑰是 123456。 因為有這個金鑰的存在,所以即便呼叫方偷偷的修改了前兩部分的內容,在驗證環節就會出現簽名不一致的情況,所以保證了安全性。
在實現過程中,遇到了這樣一個問題:如果使用 RS256 這類非對稱加密演算法,加密出來的是一串二進位制資料,所以第三部分還是用 Base64 編碼了一層,這樣最終的 JWT 就是可讀的了。
為什麼用它
1. Stateless 無狀態 ,一方面可以有效減少服務端儲存 Session 的負載;另一方面可以方便的進行擴平臺的橫向擴充套件,如 SSO 單點授權。
2. 可以有效攜帶 必要但不敏感 的資訊,且是 JSON 這種非常通用的格式。
一般我們都拿它和傳統的基於 Session-Cookie 的管理方式進行大致比較。
傳統的基於 Session 的會話管理邏輯大致如下時序圖所示:
相比較而言,傳統的 Session-Cookie 方式會有幾點問題:
1. 頻繁查詢 Session 的開銷過大。 因為 Session 儲存在服務端,大部分介面的請求都需要查詢 Session 以獲取對應使用者身份。不管是儲存在持久層(資料庫)還是記憶體中,頻繁查詢帶來的壓力會隨著使用者量的上升而急劇增大。
2. 不支援跨域,可擴充套件性差。 舉個例子:假設網站A和網站B的使用者資料是共享的,當用戶在網站A上登入以後,我們希望在訪問網站B時也保持登入狀態。這個時候就會出現一個情況:生成這個 SessionID 的伺服器並不是驗證這個 SessionID 的伺服器,也就出現了跨域身份認證的問題。除非我們把身份認證的資料也共享,即將 Session 放在持久層單獨儲存,統一管理,這樣就能在多域名下共享了,但是這樣做的成本有點高。
3. 安全性較差。 Session 放在 Cookie 中容易被 CSRF 攻擊,而且在多域名的業務場景下需要額外的做相容性處理,容易出現安全漏洞。
Security
1. 因為 JWT 的前兩個部分僅是做了 Base64 編碼處理並非加密,所以在存放資料上不能存放敏感資料。
2. 用來簽名/加密的金鑰需要妥善儲存。
3. 儘可能採用 HTTPS,確保不被竊聽。
4. 如果存放在 Cookie 中則強烈建議開啟 Http Only,其實官方推薦是放在 LocalStorage 裡,然後通過 Header 頭進行傳遞。
Cookie 的 HTTP Only 這個 Flag 和 HTTPS 並不衝突,你會發現其實還有一個 Secure 的 Flag,這個就是指 HTTPS 了,這兩個 Flag 互不影響的,開啟 HTTP Only 會導致前端 JavaScript 無法讀取該 Cookie,更多的是為了防止 類 XSS 攻擊。
一些問題和思考
JWT 的缺點其實也蠻多的,適不適用得具體看業務場景,哪個優勢更大用哪個。(一點感悟:在寫這篇文章前一直是 JWT 的堅定擁護者,越寫越發現其實傳統的 Session-Cookie 方案挺好的,很成熟。它們兩者都有優缺點,選型上要多思考斟酌才行。)
1. 資料臃腫
因為 payload 只是用 Base64 編碼,所以一旦存放資料大了,編碼之後 JWT 會很長,cookie 很可能放不下,所以還是建議放 LocalStorage,但是每次 HTTP 請求都帶上這個 臃腫的 Header 開銷也隨之變大 。
2. 無法廢棄和續簽
1. 如果有效期設定過長,意味著這個 Token 洩漏後可以被長期利用,危害較大,所以一般我們都會設定一個較短的有效期。由於有效期較短,意味著需要經常進行 重新授權 的操作。
2. 假設在使用者操作過程中升級/變更了某些許可權,勢必需要 重新整理 以更新資料。
要解決這個問題,需要在服務端部署額外邏輯,常見的做法是增加重新整理機制和黑名單機制,通過 Refresh Token 重新整理 JWT,將需要廢棄的 Token 加入到黑名單。
你的在看,我都認真當成了喜歡