只要你做過或除錯過登入系統,就會遇到 JWT —— 一個在用戶端與伺服器間傳遞、用來證明「你是誰」的精簡 token。這篇說明它如何運作,以及如何安全地解讀。
你可以自己解一個,不需要密鑰。 把 JWT 官方文件的示範 token 貼進我們的 JWT 解碼器,它會把兩個可讀的部分還給你 —— 標頭
{ "alg": "HS256", "typ": "JWT" }與內容{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }—— 而你完全沒提供任何密鑰。這是關於 JWT 最重要的一件事:那些聲明是明文傳輸的。簽章證明的是「這個 token 由持有密鑰的人簽發、之後沒被改過」,那和「機密性」是完全不同的保證。我們的解碼器在你的瀏覽器裡跑,所以你可以放心貼真實 token,它不會離開你的機器。
三個部分
JWT 是三段以點號連接的 Base64URL 字串:header.payload.signature。
- Header(標頭) —— 中繼資料,主要是簽章演算法,例如
{"alg":"HS256","typ":"JWT"}。 - Payload(內容) —— claims,一個關於使用者與 token 的 JSON 物件。
- Signature(簽章) —— 對 header 與 payload 的密碼學簽章,用密鑰(或私鑰)產生,用來偵測竄改。
你可以用 JWT 解碼器拆開並讀出前兩段。
常見 claims
Payload 裝的是 claims,部分是標準化的:
sub—— 主體(通常是使用者 ID)。iss—— 簽發者(誰建立了這個 token)。iat—— 簽發時間(Unix 時間戳)。exp—— 到期時間(Unix 時間戳)。過了就該拒絕這個 token。aud—— 預期的接收對象。
再加上你的應用自訂的 claims(角色、email 等)。
JWT 如何用於驗證
典型的無狀態流程:你登入,伺服器回傳一個已簽章的 JWT,你的用戶端在每次請求都帶著它(通常放在 Authorization: Bearer … 標頭)。伺服器驗證簽章並檢查 exp —— 有效的話,就信任其中的 claims,不必查資料庫。這種無狀態正是它的主要賣點。
是簽章,通常不是加密
多數 JWT 是簽章(JWS),不是加密。簽章證明完整性與真實性,但 payload 只是 Base64URL 編碼 —— 任何人都能讀。所以:
- 絕不把機密放進 payload(密碼、API 金鑰、隱私資料)。
- 加密是另一回事(JWE),而且少見得多。
JWT 的大部分困惑就是在這裡解開的,而它是用輸出解開的。我們把 JWT 規格書裡的示範 token 貼進我們自己的 JWT 解碼器,沒有提供任何形式的密鑰。它回傳了標頭
{
"alg": "HS256",
"typ": "JWT"
}
與內容
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
沒有任何東西被解密,因為本來就沒有東西被加密。這兩段是 Base64url 文字,而簽章保護的是它們不被修改,不是不被閱讀。任何拿到這個 token 的人都讀得到裡面每一項聲明 —— 這就是為什麼 JWT 絕對不該裝任何你不願意放進網址的東西。
真正重要的安全規則
- 一律在伺服器端用你的密鑰/公鑰驗證簽章。 解碼(像本工具做的)不等於驗證。
- 檢查
exp。 過期的 token 必須拒絕。 - HS256 vs RS256: HS256 用一組共享密鑰;RS256 用私鑰簽、公鑰驗 —— 當多個服務需要驗證但不簽發時更合適。
- 絕不把真正的密鑰貼進線上工具。 解碼 token 檢視 claims 沒問題;驗證需要密鑰,應該放在你自己的程式裡。
解讀一個 token
要除錯驗證問題 —— token 過期了嗎?角色對嗎? —— 就解碼它、檢視 payload。免費的 JWT 解碼器會顯示 header 與 payload,並把 exp 換成可讀時間,全程在你的瀏覽器本機進行,token 不會上傳。
這也順便解決了「什麼可以寫進 log」這個實務問題。{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } 可以印出來 —— 對任何拿到 token 的人來說它本來就是可讀的。但第三段不行:它是唯一能證明前兩段沒被改過的部分,而把完整的 token 貼進 bug 回報,等於在它過期之前把一份可用的憑證交出去。
解碼範例
拿這個廣為流傳的範例 token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
把前兩段解碼會得到:
// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
第三段是簽章 —— 它沒辦法「讀」,只用來證明前兩段沒被更動。
HS256 vs RS256 速覽
| HS256 | RS256 | |
|---|---|---|
| 家族 | HMAC-SHA256(對稱式) | RSA-SHA256(非對稱式) |
| 金鑰 | 一組共享密鑰 | 私鑰簽章、公鑰驗證 |
| 誰能產生有效 token | 任何持有密鑰的人 | 只有握有私鑰的人 |
| 最適合 | 同一個服務簽發又驗證 | 多個服務驗證、單一服務簽發 |
處理 exp 與 iat
exp 與 iat 是 Unix 時間戳 —— 自 1970-01-01 UTC 起算的「秒」數,不是毫秒。上面的 1516239022 是 2018-01-18。可用 Unix 時間戳轉換器換算;若對這串數字不熟,可看 Unix 時間戳指南。
另外兩個安全陷阱
- 拒絕
alg: none,並固定你預期的演算法 —— 否則攻擊者可以拿掉簽章,或把 RS256 降級成 HS256。 - JWT 在
exp之前很難撤銷。 把有效期設短,敏感操作改用 refresh token。
記住:本站的解碼器只「解碼」,從不驗證。在你的伺服器驗過簽章之前,請把「已解碼但未驗證」的 payload 一律當成不可信。