淺入淺出 JWT
JWT ( JSON Web Token )正如其名是一種將 JSON 作為 Token 來使用的構想,傳統的 Token 單純只為了身份驗證,但 JWT 因為是 JSON 而可以攜帶額外資訊在身上,所以適合用於分散式的系統。
Token
Token 中文意思是象徵、標示(中國稱令牌),是種身份驗證的機制。
大部分的系統在做與個人隱私有關的操作時往往因安全考量,要求提供帳號與密碼
但如果系統每次都要求使用者提供帳號與密碼的話,容易讓使用者感到麻煩。
而 Token 類似臨時的身份證,需由官方( Server )發給會員( Client )
當你出示 Token 給官方時就不必在提供帳號密碼等資料,因為證書提供了保證。
Token 依據規範通常是放在 HTTP 請求的 Authorization Header ,還會加上 Bearer 前綴,比如
Authorization: Bearer <YOUR_TOKEN>
JWT 不是 JWT
多數人所熟知的 JWT 大多長得像:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiTmVrbyIsImFnZSI6NjZ9.rRtZGDRTwa5jexCmki-lojh4m8plXNQnZPxI8vKh4g0
但實際上這是被稱為 JWS ( JSON Web Signature )的格式。
關於 JSON 充當 Token 的規範其實共有五份:
皆是由 IETF 的 JOSE ( Javascript Object Signing and Encryption )所制訂的 RFC 標準。
由於大多數人使用的只是 JWS 格式,而且比起 JWS 來說 JWT 更明顯地表示出是以 JSON 來當 Token 使用,再加上為了避免額外資訊帶來的困擾,介紹時通常就會省略其它不常使用的規範,久而久之對於 JWT 的印象就固定下來。
雖然沒想平反這種錯誤印象,但還是會稍微地介紹每種規格。
JWA
JWA 定義了 JOSE 中 JWS 、 JWE 與 JWK 相關的加密演算法。
JWA 規定了幾種演算法是必須提供實作,也要求它們的代稱必須一致,而 JWS 與 JWE 之間的規範不同,以下為 JWS 演算法規範:
代稱(名稱) | 演算法 | 是否提供 |
---|---|---|
HS256 | HMAC - SHA-256 | 必須 |
HS384 | HMAC - SHA-384 | 可選 |
HS512 | HMAC - SHA-512 | 可選 |
RS256 | RSASSA-PKCS1-v1_5 - SHA-256 | 建議 |
RS384 | RSASSA-PKCS1-v1_5 - SHA-384 | 可選 |
RS512 | RSASSA-PKCS1-v1_5 - SHA-512 | 可選 |
ES256 | ECDSA - P-256 & SHA-256 | 強烈建議 |
ES384 | ECDSA - P-384 & SHA-384 | 可選 |
ES512 | ECDSA - P-521 & SHA-512 | 可選 |
PS256 | RSASSA-PSS - SHA-256 & MGF1 - SHA-256 | 可選 |
PS384 | RSASSA-PSS - SHA-384 & MGF1 - SHA-384 | 可選 |
PS512 | RSASSA-PSS - SHA-512 & MGF1 - SHA-512 | 可選 |
none | 無 | 可選 |
none 演算法即不防偽,正常情況是不會使用的。官方建議如果函示庫有提供此實作,應給予一個是否開啟的選項讓使用者決定允不允許 none
JWK
JWK 定義了加密演算法中使用到的 Key 資料結構,視為一種 JSON 即可,例如:
1 | { |
這個 JWK 擁有橢圓曲線加密( ECC )類的參數 crv
、 x
、 y
,並有規定的 kty ( Key Type )與 kid ( Key ID )屬性。
JWT
JWT 定義了幾種特別的屬性,主要是給 JWS ( Payload )與 JWE ( Ciphertext )的額外資訊使用,但都是可選屬性,不一定得使用在 JWS 或 JWE 中。
規範中將這些屬性稱為 Claims ,以下為官方要求:
屬性名稱 | 屬性型態 | 意思 |
---|---|---|
iss | String | Token 發行者 |
sub | String | 主題,屬性值應為唯一 |
aud | String / String Array | Token 接收方,最少要包含發行者否則 Token 視為無效 |
exp | NumberDate | 過期時間,超過時 Token 應視為無效 |
nbf | NumberDate | 生效時間,未到時 Token 應視為無效 |
iat | NumberDate | 發行時間,此 Token 建立的時間 |
jti | String | JWT ID ,如果有多個發行者應確保不會有 ID 衝突 |
形態的 String 須為大小寫敏感; NumberDate 是自
1970-1-1T00:00:00Z UTC
開始的秒數
除了用於額外資訊的屬性, JWT 也定義了提供給 JWS 與 JWE 使用的 JOSE Header 共用屬性,總共兩個:
- typ
- cty
這兩個都是指 Media Type ,差別在於 typ 指的是 Token ; cty 指的是額外資訊,當然這兩個屬性也是可選,不過除非已經知道 Token 是 JWT ,否則應提供 typ 來標示此 Token 為 JWT 。
官方建議屬性值應去掉
application/
的前綴,比如application/xml
應直接寫xml
,如果使用大小寫不敏感的做法,官方強烈建議 jwt 應全使用大寫的 JWT
JWS
JWS 定義了如何簽名 Token 以防止內容資料被竄改。
表示法有 Compact (精簡) 與 JSON 格式,前者是最常用的而後者幾乎沒人用過,以下是例子:
1 | * Compact |
JSON 格式就只是 JSON 轉換為字串,不知道是因為太過智障還是難看,大多數的人都會使用 Compact 。
JWS 總共由三個部分組成(皆須透過 Base64URL 來編碼):
- JOSE Header
- Payload
- Signature
JOSE Header 的部分不只 JWT 定義的屬性還有 JWS 自定義的值,其中除了 alg 以外大多是可選的故不額外介紹,想知道還有哪些請點此。
alg 表示此 JWS 用來簽名的加密演算法,值應為 JWA 所定義的代稱
最簡單的 Header 看起來應為:
1 | { |
而 Payload 就是一個使用者自訂的 JSON ,好比說:
1 | { |
Signature 是將上述的兩個部分先透過 Base64URL 編碼,接著以 .
組成作為演算法輸入值來計算出來的,請參考下列虛擬碼:
1 | header = {typ: "JWT", alg: "HS256"} |
取得完三個部分後,透過 .
連接起來就會得到 JWS Compact 的 Token :
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAxMCwibmFtZSI6Ik5la28iLCJhZ2UiOjY2fQ.B6VQtIs8BRUIxTZHmIcR3fQ3asJlgT7bt8AfxmbP650
JWE
JWS 其實很明顯可以看出資訊是公開的,畢竟只有透過 Base64URL 編碼過,相當容易就可以透過解碼取得資料內容,對於真正需要隱藏的資料,應該使用 JWE 。
JWE 由五個部分組成,所以兩個的 Token 很明顯可以看出不同:
- JOSE Header
- Encrypted Key
- Initialzation Vector
- Ciphertext
- Authentication Tag
其實很明顯可以看出有加密用的公鑰與初始向量等資訊在其中,我並沒有仔細看 JWE 的規範所以對他真的有興趣者請參考之前提供的規範網址或是到 O3noBlog - JSON Web Token 。
適合的情況
JWT 能夠額外攜帶資訊這點使其適合用於分布式系統,除了可以省略部分資料同步的困擾外,也能某種程度上減少 I/O 操作。
如果以修改使用者資料來看,傳統的 Token 需要做 2 次資料庫 I/O :
但如果是 JWT 則只需要做 1 次資料庫 I/O :
或是基於有額外資訊這點, JWT 也相當適合用於一次性認證,比方說帳號認證、忘記密碼等。
問題與建議
不過也有些很明顯的缺點,好比說 JWT 是無法被註銷,當 Server 簽發出去就只能等到過期( exp ),沒有方法可以藉由 Server 來註銷。
此外 JWS 的 Payload 相當於公開,所以也不該在裡面放較為隱私的資訊,再考慮到 URL 長度限制或 Cookie 大小( 4K ), JWT 也不應該太長。
綜上所述,在使用 JWT 機制時應確保:
- 不放敏感資料
- 不放常變動的資料(否則得不斷生成新的 Token )
- 設置 exp 且時間不得太長
- 設置 jti 或 exp 來改變生成的 JWT ,極端點可以每次都換密鑰
- Server 應維護 Active Token 列表或是黑名單列表來控制 JWT
關於一些錯誤印象
很多人都覺得 JWT 拯救了世界,其實沒有。
比如說有人覺得 JWT 可以防止 CSRF ( Cross Site Request Forgery )攻擊,但 CSRF 運作是透過 JavaScript 來運作的(當然還有手賤亂點),是 Token 就沒辦法避免攻擊。
或有人覺得 JWT 更加安全,但這也沒有,畢竟 Token 就是 Token ,況且 JWT 要偽造是有可能的,一旦 Server 端的加密密鑰被破解,對方想怎麼偽造就怎麼偽造,這樣看來還比傳統的更不安全。
最後還有人覺得可以充當 Session ,這個是最嚴重的,千萬不要把 JWT 當作 Session 使用。
Session 是由 Server 來保存 Client 狀態物件的機制,概念是把 SessionID 交給 Client 讓它每次請求都帶上,這樣 Server 就可以辨識出 Client 當前的狀態,許多購物車就是這樣實作
因為 Session 有幾種問題存在:
- 額外耗費 Server 資源(創建與管理 Session 物件)
- 分散式系統得處理 Session 共享與傳遞
- 只認 ID 可能導致被盜用身份
但使用 JWT 不會使情況變好甚至更糟,光是前面提過的無法銷毀就比不上 Session ,雖然也許可以透過某些機制來處理,但除非時間很多我不會建議自己研究處理機制, Session 早已行之有年,上述的問題可能早存在一堆解法,不但有經過他人的實驗也有保障,絕對比起 JWT 更好。