JWT 解析:標頭、酬載、簽名、base64url,以及為何解碼不等於驗證

三個點:JSON Web Token 的結構

JSON Web Token 是由三個以點號分隔的 base64url 編碼值組成的字串:base64url(標頭).base64url(酬載).base64url(簽名)。RFC 7519 於 2015 年 5 月發布,定義了這一格式。無論 JWT 出現在 Authorization 標頭、Cookie 還是 URL 查詢參數中,都遵循這個結構。三個部分可以獨立解碼,而簽名部分以密碼學保證將它們緊密連結。

這種格式的設計目標是緊湊且 URL 安全。在 JWT 出現之前,API 認證通常依賴伺服器端儲存的不透明會話識別符,每次請求都需要資料庫查詢。JWT 將宣告直接嵌入令牌中,使無狀態伺服器無需查詢即可驗證。這種便利性伴隨著責任:與伺服器可以立即撤銷的會話令牌不同,已簽名的 JWT 在其 exp 宣告過期前始終有效,除非伺服器維護黑名單。

base64url 編碼:base64 的 URL 安全變體

標準 base64 使用 64 個字元對二進位資料進行編碼:A–Z、a–z、0–9、+/,並以 = 填充。其中兩個字元(+/)在 URL 和 HTTP 查詢字串中具有特殊意義,若 base64 字串出現在 URL 中則需要百分比編碼。base64url 定義於 RFC 4648 第 5 節,透過將 + 替換為 -/ 替換為 _ 並完全省略尾部的 = 填充來解決這個問題。這樣的結果可以直接用於 URL、HTTP 標頭和 Cookie 中,無需進一步編碼。

膨脹率與標準 base64 相同:每 3 個輸入位元組變為 4 個 base64url 字元——約 33% 的擴展比例。由於編碼是可逆的且不需要任何金鑰,任何獲得 JWT 的人都可以透過解碼讀取其標頭和酬載——base64url 是編碼,不是加密

標頭:演算法選擇與令牌類型

標頭是一個 JSON 物件,告知接收方如何處理令牌。最基本的標頭包含兩個欄位:alg(簽名演算法)和 typ(標準令牌固定為 "JWT")。RFC 7518 定義了演算法識別符。最常見的有:HS256——HMAC-SHA256,一種對稱演算法,使用同一個金鑰進行簽名和驗證;RS256——帶 SHA-256 的 RSA-PKCS1v1.5,非對稱,用私鑰簽名,用對應的公鑰驗證;ES256——使用 P-256 和 SHA-256 的 ECDSA,也是非對稱,但簽名比 RSA 更短。

對稱與非對稱演算法的選擇有實際的架構影響。使用 HS256 時,所有需要驗證令牌的服務都必須擁有共享金鑰,洩露給一個服務等同於洩露給所有服務。使用 RS256 或 ES256 時,發行方保持私鑰私密,只公開公鑰(通常透過 JWKS 端點),驗證服務不需要接觸簽名材料。對於多服務或第三方整合,非對稱演算法更為合適。

酬載:已登錄宣告及其含義

酬載是包含宣告的 JSON 物件——關於實體的陳述,通常是已認證的使用者,以及令牌本身的元資料。RFC 7519 第 4.1 節定義了七個已登錄宣告名稱,其語義已標準化:iss(發行方)、sub(主體,令牌所涉及的主體,通常是使用者 ID)、aud(受眾,令牌的預期接收方)、exp(過期時間,超過此 NumericDate 後令牌不得被接受)、nbf(生效時間,在此 NumericDate 之前令牌不得被接受)、iat(發行時間)以及 jti(JWT ID,防止重放攻擊的唯一識別符)。

NumericDate 是自 1970-01-01T00:00:00Z UTC(Unix 紀元)以來的秒數,不是毫秒。熟悉 JavaScript Date.now()(返回毫秒)的開發者在編寫令牌驗證程式時若未進行轉換,這是常見的錯誤來源。標準 JWT 的酬載已簽名但未加密——任何截獲令牌的人都可以讀取所有酬載宣告。若酬載包含敏感資料,應使用 JWE(RFC 7516)進行加密,或僅透過 TLS 傳輸。

簽名:它保護什麼——以及 alg:none 的陷阱

簽名計算基於簽名輸入,即 base64url(標頭) + "." + base64url(酬載)。對於 HS256,簽名是 HMAC-SHA256(金鑰, 簽名輸入) 再進行 base64url 編碼。由於簽名輸入包含標頭和酬載兩者,對任一部分的任何修改——即使只改變一個字元——都會使簽名無效。正確驗證簽名的伺服器可以確定令牌自發行以來未被篡改。

2015 年,多個 JWT 函式庫中發現了一類廣為人知的漏洞:alg:none 攻擊。RFC 7519 在技術上允許 "alg":"none" 表示無簽名的未受保護 JWT。缺陷在於一些函式庫從(未驗證的)標頭讀取 alg 欄位來選擇驗證路徑——如果攻擊者將標頭改為 "alg":"none" 並移除簽名,函式庫會在不檢查任何簽名的情況下宣告令牌有效。CVE-2015-9235 記錄了流行的 node-jsonwebtoken 函式庫中的這個問題。解決方案是:驗證方必須明確指定預期的演算法,並拒絕標頭中使用不同演算法的任何令牌。

解碼與驗證:為何這個區別至關重要

解碼 JWT 是指對三個部分進行 base64url 解碼,以恢復 JSON 標頭、酬載和原始簽名位元組。不需要任何金鑰。任何持有 JWT 字串的人都可以解碼並讀取宣告。這是設計使然——JWT 並非不透明容器。調試認證問題的開發者、讀取存取日誌的運維工程師、稽核系統的安全研究員:所有人都可以解碼任何他們擁有的 JWT。這正是 JWT 解碼工具有價值的原因。

驗證 JWT 是指檢查簽名是否由正確的金鑰對精確的標頭和酬載位元組產生。驗證成功可以證明兩件事:令牌由簽名金鑰持有者創建(真實性),以及自簽名以來標頭和酬載均未被更改(完整性)。驗證需要金鑰——HMAC 演算法需要共享金鑰,RSA/ECDSA 需要公鑰。

關鍵錯誤是將解碼的 JWT 視為可信而不進行驗證。如果客戶端從 localStorage 中讀取 JWT 宣告並直接使用,而不進行伺服器端簽名驗證,就是在信任任何使用者都可以自由偽造的資料。攻擊者可以製作 {"sub":"admin","role":"superuser"},用任意標頭進行 base64url 編碼,並附上偽造或空簽名——這樣的令牌完全可以被解碼。授權決策必須在伺服器端進行密碼學驗證之後做出。 JWT 解碼工具是檢查和調試的正確工具,但永遠不能替代生產應用中的伺服器端驗證。