你可能沒那么了解 JWT

0)前言
最近在開發(fā)一個統(tǒng)一認(rèn)證服務(wù),涉及到 OIDC 協(xié)議,其中授權(quán)碼模式所頒發(fā)的 id_token 使用的是 JWT ( JSON Web Token ) ,因為這次使用的庫的默認(rèn)簽名算法和以往不同,所以特地去翻閱了 JWT 的 RFC 文檔( RFC 7519[1] ),一番閱讀后發(fā)現(xiàn)原來對 JWT 的認(rèn)知只停留在表面,還有更多深層的內(nèi)容是不知道的。
1)我們最常使用的 JWT
每次提到無狀態(tài)的 JWT 時相信都會看到另一種基于 Session 的用戶認(rèn)證方案介紹,這里也不例外,Session 的認(rèn)證流程通常會像這樣:

這種方案有一些缺點:
需要從內(nèi)存或數(shù)據(jù)庫里存取 session 數(shù)據(jù) 擴展性差,對于分布式應(yīng)用,需要實現(xiàn) session 數(shù)據(jù)共享
JWT 正好可以解決這些問題:

JWT 的魔法很簡單,將需要使用到的用戶數(shù)據(jù)等信息放入 JWT 里面,每次請求都會攜帶上,只要保證密鑰不泄露,JWT 就無法偽造。
一個簡單的 JWT 示例如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIyMDIxLTEwLTI0IDAwOjAwOjAwIiwibmFtZSI6InRvZ2V0dG95b3UifQ.XdF46NflSUjnt-adAc6rNZEXI1OD6nxtwGuhz9qkxUA
jwt.io[2] 這個網(wǎng)站相信沒有人不知道了,把上面的 JWT 復(fù)制粘貼到網(wǎng)站中

可以看出 JWT 以不同顏色區(qū)分,兩個小數(shù)點隔開,分為了三部分:
① Header(頭部):JSON 對象,描述 JWT 的元數(shù)據(jù)。其中 alg 屬性表示簽名的算法(algorithm),默認(rèn)是 HMAC SHA256(寫成 HS256);typ 屬性表示這個令牌(token)的類型(type),統(tǒng)一寫為 JWT
② Payload(載荷):JSON 對象,存放實際需要傳遞的數(shù)據(jù),支持自定義字段
③ Signature(簽名):這部分就是 JWT 防篡改的精髓,其值是對前兩部分 base64UrlEncode 后使用指定算法簽名生成,以默認(rèn) HS256 為例,指定一個密鑰(secret),就會按照如下公式生成:
HMACSHA256(
?base64UrlEncode(header)?+?"."?+?base64UrlEncode(payload),
?secret,
)
到這里,大多數(shù)人對 JWT 的認(rèn)知應(yīng)該是停留在此了,日常使用也已經(jīng)足夠,但你想更深入了解 JWT 的話,那你就得知道 JOSE 。
2)JOSE 規(guī)范
什么是 JOSE ,它和 JWT 之間又有什么關(guān)系呢。
JOSE 全稱 JSON Object Signing and Encryption ( RFC 7165[3] , RFC 7520[4] ),它定義了一系列的標(biāo)準(zhǔn),用來規(guī)范網(wǎng)絡(luò)傳輸過程中使用 JSON 的方式,我們上面一直說的 JWT 其實是 JOSE 體系之一。

其中 JWT 又可分為 JWS 和 JWE 兩種不同的實現(xiàn),我們大部分日常所使用的,所說的 JWT 其實應(yīng)該屬于 JWS 。 為什么這么說,請看下文。
3)JWA 和 JWS 以及 JWK
JWA 的全稱是 JSON Web Algorithms ( RFC 7518[5] ) ,字如其名, JOSE 體系中涉及到的所有算法就是它來定義的,比如通用算法有 Base64-URL 和 SHA,簽名算法有 HMAC,RSA 和 Elliptic Curve(EC 橢圓曲線),本文不會深入到算法原理(我也不懂),只是想讓你知道 JWA 是做什么的。我們上面的 JWT 例子中第一部分 Header 有個 alg 屬性,其值是 HS256 ,也就是 HMAC + SHA256 算法。
說了那么多,好像都沒有正式介紹過 JWS 。JWS 的全稱是 JSON Web Signature ( RFC 7515[6] ) ,它的核心就是簽名,保證數(shù)據(jù)未被篡改,而檢查簽名的過程就叫做驗證。更通俗的理解,就是對應(yīng)前面提到的 JWT 的第三部分 Signature ,所以我才會說我們?nèi)粘K褂玫?JWT 都是 JWS 。
通常在客戶端-服務(wù)端模式中,JWS 使用 JWA 提供的 HS256 算法加上一個密鑰即可,這種方式嚴(yán)格依賴密鑰,但在分布式場景,可能多個服務(wù)都需要驗證 JWT ,若要在每個服務(wù)里面都保存密鑰,那么安全性將會大打折扣,要知道,密鑰一旦泄露,任何人都可以隨意偽造 JWT 。
解決辦法就是使用非對稱加密算法 RSA ,RSA 有兩把鑰匙,一把公鑰,一把私鑰,可以使用私鑰簽發(fā)(簽名分發(fā)) JWT ,使用公鑰驗證 JWT ,公鑰是所有人都可以獲取到的。這樣一來,就只有認(rèn)證服務(wù)保存著私鑰,進行簽發(fā),其他服務(wù)只能驗證。
如下是一個使用 RS256 ( RSA + SHA256 ) 算法生成的 JWT :
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjF6aXlIVG15M184MDRDOU1jUENHVERmYWJCNThBNENlZG9Wa3VweXdVeU0ifQ.eyJqdGkiOiIzWUJ5eWZ2TDB4b01QNXdwTXVsZ0wiLCJzdWIiOiI2MDE5NDI5NjgwMWRjN2JjMmExYjI3MzUiLCJpYXQiOjE2MTI0NDQ4NzEsImV4cCI6MTYxMzY1NDQ3MSwic2NvcGUiOiJvcGVuaWQgZW1haWwgbWVzc2FnZSIsImlzcyI6Imh0dHBzOi8vc3RlYW0tdGFsay5hdXRoaW5nLmNuL29pZGMiLCJhdWQiOiI2MDE5M2M2MTBmOTExN2U3Y2IwNDkxNTkifQ.cYyZ6buwAjp7DzrYQEhvz5rvUBhkv_s8xzuv2JHgzYx0jbqqsWrA_-gufLTFGmNkZkZwPnF6ktjvPHFT-1iJfWGRruOOMV9QKPhk0S5L2eedtbKJU6XIEkl3F9KbOFwYM53v3E7_VC8RBj5IKqEY0qd4mW36C9VbS695wZlvMYnmXhIopYsd5c83i39fLBF8vEBZE1Rq6tqTQTbHAasR2eUz1LnOqxNp2NNkV2dzlcNIksSDbEGjTNkWceeTWBRtFMi_o9EWaHExdm5574jQ-ei5zE4L7x-zfp9iAe8neuAgTsqXOa6RJswhyn53cW4DwWg_g26lHJZXQvv_RHZRlQ
把它復(fù)制到 jwt.io 上面看看

注意我綠色框選中的地方,里面是一段 JSON ,我們把它刪掉,看看輸入框的提示信息

這里提示了,里面是填寫公鑰格式(通常為 PEM)或者 JWK (我們說過 RSA 算法是使用私鑰簽發(fā) JWT,公鑰進行驗證),剛剛我們刪掉的是一段 JSON,所以必然不是公鑰格式,那是 JWK 嗎?
當(dāng)然是,JWK 的全稱是 JSON Web Key ( RFC 7517[7] ) ,它就是一個 JSON ,JWK 就是用 JSON 來表示密鑰(JSON 字段因密鑰類型而異)。例如剛才刪除的 JWK :
{
??"e":?"AQAB",
??"kty":?"RSA",
??"n":?"wVKQLBUqOBiay2dkn9TlbfuaF40_edIKUmdLq6OlvzEMrP4IDzdOk50TMO0nfjJ6v5830_5x0vRg5bzZQeKpHniR0sw7qyoSI6n2eSkSnFt7P-N8gv2KWnwzVs_h9FDdeLOeVOU8k_qzkph3_tmBV7ZZG-4_DEvgvat6ifEC-WzzYqofsIrTiTT7ZFxTqid1q6zrrsmyU2DQH3WdgFiOJVVlN2D0BuZu5X7pGZup_RcWzt_9T6tQsGeU1juSuuUk_9_FVDXNNCTObfKCTKXqjW95ZgAI_xVrMeQC5nXlMh6VEaXfO83oy1j36wUoVUrUnkANhp-dnjTdvJgwN82dGQ"
}
其中 kty 字段是必須的,代表密鑰類型,支持 EC 橢圓曲線密鑰,RSA 密鑰和 oct 對稱密鑰。
JWK 和 公鑰格式 Pem 是可以互相轉(zhuǎn)換的:

我們現(xiàn)在已經(jīng)知道,驗證這個 JWT 是需要公鑰或 JWK 的,那你會不會好奇 jwt.io 這個網(wǎng)站是怎么知道 JWK 的呢,為什么一粘貼,就自動將 JWK 填充進去了。
原理其實很簡單,而且已經(jīng)是一種大家都遵循的規(guī)范了,就是將 JWK 放在 iss/.well-known/jwks.json 下,其中 iss 就是 Payload 里面的 iss 。


當(dāng)你在 jwt.io 粘貼下 JWT 的瞬間,jwt.io 會先解析 Header ,判斷出 JWT 使用的算法(JWA),接著解析出 Payload 的信息,由于這里是 RS256 算法, 所以還會去請求 Payload 里的 iss 下的 .well-known/jwks.json得到 JWK ,從而完成 JWS 的驗證。
4)另一種 JWT 的實現(xiàn) :JWE
我們說過,經(jīng)過 Signature 簽名后的 JWT 就是指的 JWS ,而 JWS 僅僅是對前兩部分簽名,保證無法篡改,但是其 Payload(載荷) 信息是暴露的(只是作了 base64UrlEncode 處理)。因此,使用 JWS 方式的 Payload 是不適合傳遞敏感數(shù)據(jù)的,JWT 的另一種實現(xiàn) JWE 就是來解決這個問題的。
JWE 全稱是 JSON Web Encryption ( RFC 7516[8] ) ,JWS 的 Payload 是 Base64Url 的明文,而 JWE 的數(shù)據(jù)則是經(jīng)過加密的。它可以使 JWT 更加安全。
JWE 提供了兩種方案:共享密鑰方案和公鑰/私鑰方案。共享密鑰方案的工作原理是讓各方都知道一個密鑰,大家都可以簽名驗證,這和 JWS 是一致的。而公鑰/私鑰方案的工作方式就不同了,在 JWS 中私鑰對令牌進行簽名,持有公鑰的各方只能驗證這些令牌;但在 JWE 中,持有私鑰的一方是唯一可以解密令牌的一方,公鑰持有者可以引入或交換新數(shù)據(jù)然后重新加密,因此,當(dāng)使用公鑰/私鑰方案時,JWS 和 JWE 是互補的。
想要理解這一點的更簡單的方法是從生產(chǎn)者和消費者的角度進行思考。生產(chǎn)者對數(shù)據(jù)進行簽名或加密,消費者可以對其進行驗證或解密。對于 JWS ,私鑰對 JWT 進行簽名,公鑰用于驗證,也就是生產(chǎn)者持有私鑰,消費者持有公鑰,數(shù)據(jù)流動只能從私鑰持有者到公鑰持有者。相比之下,對于 JWE ,公鑰是用于加密數(shù)據(jù),而私鑰用來解密,在這種情況下,數(shù)據(jù)流動只能從公鑰持有者到私鑰持有者。如下圖所示(來源 JWT Handbook[9] ):

相比于 JWS 的三個部分,JWE 有五個部分組成(四個小數(shù)點隔開)。一個 JWE 示例如下:
eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKxYxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTPcFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw
Protected Header (受保護的頭部) :類似于 JWS 的 Header ,標(biāo)識加密算法和類型。 Encrypted Key (加密密鑰) :用于加密密文和其他加密數(shù)據(jù)的密鑰。 Initialization Vector (初始化向量) :一些加密算法需要額外的(通常是隨機的)數(shù)據(jù)。 Encrypted Data (Ciphertext) (加密的數(shù)據(jù)) :被加密的數(shù)據(jù)。 Authentication Tag (認(rèn)證標(biāo)簽) :算法產(chǎn)生的附加數(shù)據(jù),可用于驗證密文內(nèi)容不被篡改。
這五個部分的生成,也就是 JWE 的加密過程可以分為 7 個步驟:
根據(jù) Header alg 的聲明,生成一定大小的隨機數(shù)
根據(jù)密鑰管理方式確定 Content Encryption Key ( CEK )
根據(jù)密鑰管理方式確定 JWE Encrypted Key
計算所選算法所需大小的 Initialization Vector (IV)。如果不需要,可以跳過
如果 Header 聲明了 zip ,則壓縮明文
使用 CEK、IV 和 Additional Authenticated Data ( AAD,額外認(rèn)證數(shù)據(jù) ) ,通過 Header enc 聲明的算法來加密內(nèi)容,結(jié)果為 Ciphertext 和 Authentication Tag
最后按照以下算法構(gòu)造出 Token:
base64(header)?+?'.'?+
base64(encryptedKey)?+?'.'?+?//?Steps?2?and?3
base64(initializationVector)?+?'.'?+?//?Step?4
base64(ciphertext)?+?'.'?+?//?Step?6
base64(authenticationTag)?//?Step?6
JWE 相比 JWS 更加安全可靠,但是不夠輕量,有點復(fù)雜。
5)安全性考慮
不管怎樣,JWT 多多少少還是存在一些安全性隱患的,下面是平時開發(fā)過程的一些建議:
始終執(zhí)行算法驗證
簽名算法的驗證固定在后端,不以 JWT 里的算法為標(biāo)準(zhǔn)。假設(shè)每次驗證 JWT ,驗證算法都靠讀取 Header 里面的 alg 屬性來判斷的話,攻擊者只要簽發(fā)一個 "alg: none" 的 JWT ,就可以繞過驗證了。
選擇合適的算法
具體場景選擇合適的算法,例如分布式場景下,建議選擇 RS256 。
HMAC 算法的密鑰安全
除了需要保證密鑰不被泄露之外,密鑰的強度也應(yīng)該重視,防止遭到字典攻擊。
避免敏感信息保存在 JWT 中
JWS 方式下的 JWT 的 Payload 信息是公開的,不能將敏感信息保存在這里,如有需要,請使用 JWE 。
JWT 的有效時間盡量足夠短
JWT 過期時間建議設(shè)置足夠短,過期后重新使用 refresh_token 刷新獲取新的 token 。
6)總結(jié)
今天為大家講了一些 JWT 不為人知的秘密,總結(jié)一下涉及到的知識點:
JOSE:規(guī)范網(wǎng)絡(luò)傳輸過程中使用 JSON 的一系列標(biāo)準(zhǔn) JWT:以 JSON 編碼并由 JWS 或 JWE 安全傳遞的表示形式 JWS:簽名和驗證 Token JWE:加密和解密 Token JWA:定義 JOSE 體系中涉及到的所有算法 JWK:用 JSON 來表示密鑰
最后,再次附上 JOSE 的體系圖,相關(guān)的 RFC 均備注在圖上了:

參考資料
RFC 7519: https://datatracker.ietf.org/doc/rfc7519/
[2]jwt.io: https://jwt.io/
[3]RFC 7165: https://www.rfc-editor.org/rfc/rfc7165.html
[4]RFC 7520: https://www.rfc-editor.org/rfc/rfc7520.html
[5]RFC 7518: https://www.rfc-editor.org/rfc/rfc7518.html
[6]RFC 7515: https://www.rfc-editor.org/rfc/rfc7515.html
[7]RFC 7517: https://www.rfc-editor.org/rfc/rfc7517.html
[8]RFC 7516: https://www.rfc-editor.org/rfc/rfc7516.html
[9]JWT Handbook: https://auth0.com/resources/ebooks/jwt-handbook
