JSON Web Tokens,又稱 JWT
。本文作者將詳解:為何 JWT 不適合儲存 Session,以及 JWT 引發的安全隱患。望各位使用前三思。
原文地址:http://cryto.net/~joepie91/blog/2016/06/13...
斷斷續續翻譯了一週,今天終於審校一下,算是填坑完畢。自己部落格廣告走一波:https://wi1dcard.cn/。
十分不幸,我發現越來越多的人開始推薦使用 JWT 管理網站的使用者會話(Session
)。在本文中,我將說明為何這是個非常非常差勁的想法。
為了避免疑惑和歧義,首先定義一些術語:
- 無狀態 JWT(
Stateless JWT
):包含 Session 資料的 JWT Token。Session 資料將被直接編碼進 Token 內。 - 有狀態 JWT(
Stateful JWT
):包含 Session 引用或其 ID 的 JWT Token。Session 資料儲存在服務端。 Session token
(又稱Session cookie
):標準的、可被簽名的 Session ID,例如各類 Web 框架(譯者注:包括 Laravel)內已經使用了很久的 Session 機制。Session 資料同樣儲存在服務端。
需要澄清的是:本文並非挑起「永遠不要使用 JWT」的爭論 —— 只是想說明 JWT 並不適合作為 Session 機制,且十分危險。JWT 在其它方面的確有其用武之地。本文結尾,我將簡短地介紹一些合理用途。
首先需要說明
很多人錯誤地嘗試比較 Cookies
和 JWT
。這種對比毫無意義,就像對比記憶體和硬碟一樣。Cookies 是一種儲存機制,然而 JWT Tokens 是被加密並簽名後的令牌。
它們並不對立 —— 相反,他們可以獨立或結合使用。正確的對比應當是:Session
對比 JWT
,以及 Cookies
對比 Local Storage
。
在本文中,我將把 JWT Tokens 同 Session 展開對比,並偶爾對比 Cookie
和 Local Storage
。這樣的比較才有意義。
JWT 坊間流傳的優勢
在人們安利 JWT 時,常常宣揚以下幾點好處:
- 易於水平擴充套件
- 易於使用
- 更加靈活
- 更加安全
- 內建過期時間功能
- 無需詢問使用者「本網站使用 Cookies」
- 防止 CSRF 攻擊
- 更適用於移動端
- 適用於阻止 Cookies 的使用者
我將會逐條闡述以上觀點為何是錯誤或誤導性的,其中部分解釋可能會有些模糊,這主要是因為這些「好處」的表述本身就比較模糊。你可以在文末找到我的聯絡方式,我將十分樂意對更加具體的「好處」進行分析闡述。
易於水平擴充套件?
這是列表中唯一一條在技術層面部分正確的「好處」,但前提是你使用的是無狀態 JWT Tokens。然而事實上,幾乎沒人需要這種橫向擴充套件能力。有很多更簡單的擴充方式,除非你在運維像淘寶這樣體量的系統,否則根本不需要無狀態的會話(Stateless sessions
)。
一些擴充套件有狀態會話(Stateful sessions
)的例子:
- 在單臺伺服器上執行多個後端程式:只需在此伺服器上安裝 Redis 服務用於儲存 Session 即可。
- 執行多臺伺服器:只需一臺專用的 Redis 伺服器用於儲存 Session 即可。
- 在多叢集內執行多臺伺服器:會話保持(又稱:粘滯會話)。
以上所有場景在現有軟體系統內都具備良好的支援,你的應用需要進行特殊處理的可能性基本為零。
或許你在想,應當為你的應用預留更多調整空間,以防未來需要某些特殊操作。但實踐告訴我們,以後再替換 Session 機制並不困難,唯一的代價是,在遷移後所有使用者將被強制登出一次。我們沒必要在前期實現 JWT,尤其是考慮到它所帶來的負面影響。我將在後文進行解釋。
易於使用?
這個真沒有。你不得不自行處理 Session 的管理機制,無論是客戶端還是服務端。然而標準的 Session cookies
則開箱即用,JWT 並沒有更簡單。
更加靈活?
我暫時還沒看到有人成功地闡述「JWT 如何更加靈活」。幾乎每個主流的 Session 實現,都允許你直接把資料儲存進 Session,這跟 JWT 的機制並沒有差別。據我所知,這只是個流行語罷了。如果你不同意,可以隨時帶上示例與我聯絡。
更加安全?
一大批人認為 JWT Tokens「更加安全」,理由是使用了加密技術。實際上,簽名後的 Cookies 比未簽名的 Cookies 同樣更加安全,但這絕不是 JWT 獨有的,優秀的 Session 實現均使用簽名後的 Cookies(譯者注:例如 Laravel)。
「使用加密技術」並不能神奇地使某些東西更加安全,它必須服務於特定目的,並且是針對該目的的有效解決方案。錯誤地使用加密反而可能會降低安全性。
另一個我聽過很多次的對於「更加安全」的論述是「JWT 不使用 Cookies 傳輸 Tokens」。這實在是太荒謬了,Cookie 只不過是一條 HTTP 頭資訊,使用 Cookies 並不會造成任何不安全。事實上,Cookies 受到特別良好的保護,用於防止惡意的客戶端程式碼。我將在後文進行闡述。
如果擔心有人攔截掉你的 Session cookies,那你應當考慮使用 TLS。如果不使用 TLS,任何型別的 Session 機制都可能被攔截,包括 JWT。
內建過期時間功能?
無意義,又沒什麼卵用的特性。在服務端也能實現過期控制,有不少 Session 實現就是這麼做的。實際上,服務端的過期控制更加合理,這樣你的應用就可以清除不再需要的 Session 資料;若使用無狀態 JWT Tokens 且依賴於它的過期機制,則無法執行此操作。
無需詢問使用者「本網站使用 Cookies」?
完全錯誤。並沒有什麼「Cookies 法律」—— 有關 Cookies 的各種法律實際上涵蓋了任何型別「對某項服務的正常執行非嚴格必須的永續性 ID」,任何你能想到的 Session 機制都包括在內。
譯者注:然鵝中國並沒有。
簡單來說:
- 若出於系統功能目的使用 Session 或 Token(例如:保持使用者的登入態),那麼無論怎樣儲存 Session 均無需徵得使用者同意。
- 若出於其他目的使用 Session 或 Token(例如:資料分析、追蹤),那麼無論怎樣儲存 Session 都需要詢問使用者是否允許。
防止 CSRF 攻擊?
這個真·真沒有。儲存 JWT Tokens 的方式大概有兩種:
- 存入 Cookie:仍然易受 CSRF 攻擊,還是需要進行特殊處理,保護其不受攻擊。
- 其他地方,例如 Local Storage:雖然不易受到 CSRF 攻擊,但你的網站需要 JavaScript 才能正常訪問;並且又引發了另一個完全不同,或許更加嚴重的漏洞。我將在後文詳細說明。
預防 CSRF 攻擊唯一的正確方法,就是使用 CSRF Tokens。Session 機制與此無關。
更適用於移動端?
毫無根據。目前所有可用的瀏覽器幾乎都支援 Cookies,因此也支援 Session。同樣,主流的移動端開發框架以及嚴謹的 HTTP 客戶端庫都是如此。這根本不是個問題。
適用於阻止 Cookies 的使用者?
不太可能。使用者通常會阻止任何意義上的持久化資料,而不是隻禁止 Cookies。例如,Local Storage 以及任何能夠持久化 Session 的儲存機制(無論是否使用 JWT)。不管你出於多麼簡單的目的使用 JWT 都無濟於事,這是另一個完全獨立的問題了。另外,試圖讓身份認證過程在沒有 Cookies 的情況下正常進行,基本沒戲。
最重要的是,禁用掉所有 Cookies 的多數使用者都明白這會導致身份認證無法使用,他們會單獨解鎖那些他們比較關心的站點。這並不是你 —— 一個 Web 開發者應當解決的問題。更好的方案是,向你的使用者們詳細地解釋為何你的網站需要 Cookies 才能使用。
JWT 的劣勢
以上,我已經對常見的誤解做了說明,以及為什麼它們是錯誤的。你或許在想:「這好像也沒什麼大不了的,即便 JWT 無法帶來任何好處,但也不會造成什麼影響」,那你真是大錯特錯了。
使用 JWT 作為 Session 機制存在很多缺點,其中一部分會造成嚴重的安全問題。
更費空間
JWT Tokens 實際上並不「小」。尤其是使用無狀態 JWT 時,所有的資料將會被直接編碼進 Tokens 內,很快將會超過 Cookies 或 URL 的長度限制。你可能在想將它們儲存到 Local Storage,然而...
更不安全
若將 JWT Tokens 儲存到 Cookies 內,那麼安全性與其他 Session 機制無異。但如果你將 JWT 儲存至其它地方,會導致一個新的漏洞,詳見本文,尤其是「Storing sessions」這一部分。
書接上回:Local Storage,一個 HTML5 內很棒的功能,使瀏覽器支援 Key/Value 儲存。所以我們應當將 JWT Tokens 儲存到 Local Storage 嗎?考慮到這些 Tokens 可能越來越大,或許會很有用。Cookies 通常在 4k 左右的儲存時比較佔優勢,對於較大的 Tokens,Cookies 可能無法勝任,而 Local Storage 或許成了明確的解決方案。然而,Local Storage 並沒有提供任何類似 Cookies 的安全措施。
LocalStorage 與 Cookies 不同,並不會在每次請求時傳送儲存的資料。獲取資料的唯一方法是使用 JavaScript,這意味著任何攻擊者注入的 JavaScript 指令碼只需通過內容安全策略檢查,就能任意訪問或洩露資料。不光是這樣,JavaScript 並不在意或追蹤資料是否通過 HTTPS 傳送。就 JavaScript 而言,它就只是個資料而已,瀏覽器會像操作其它資料一樣來處理它。
在歷代工程師們經歷了各種麻煩之後,終於能夠確保沒有人可以惡意接觸到我們的 Cookies,然而我們卻試圖忽略這些經驗。這對我來說似乎是在退步。
簡單來說,使用 Cookies 並不是可選的,無論你是否採用 JWT。
無法單獨銷燬
還有更多安全問題。不像 Sessions 無論何時都可以單獨地在服務端銷燬。無狀態 JWT Tokens 無法被單獨的銷燬。根據 JWT 的設計,無論怎樣 Tokens 在過期前將會一直保持有效。舉個例子,這意味著在檢測到攻擊時,你卻不能銷燬攻擊者的 Session。同樣,在使用者修改密碼後,也無法銷燬舊的 Sessions。
對此,我們幾乎無能為力,除非重新構建複雜且有狀態(Stateful
)的基礎設施來明確地檢測或拒絕特定 Session,否則將無法結束會話。但這完全違背了使用無狀態 JWT Tokens 的最初目的。
資料延遲
與上文的安全問題類似,還有另一個潛在的安全隱患。就像快取,在無狀態 Tokens 記憶體儲的資料最終會「過時」,不再反映資料庫內最新的資料。
這意味著,Tokens 內保留的可能是過期的資訊,例如:使用者在個人資訊頁面修改過的舊 URL。更嚴肅點講,也可能是個具備 admin
許可權的 Token,即使你已經廢除了 admin
許可權。因為無法銷燬這些 Tokens,所以面對需要移除的管理員許可權,除非關閉整個系統,別無他法。
實現庫缺乏生產環境驗證或壓根不存在
你或許在想,以上的這些問題都是圍繞著「無狀態 JWT」展開的,這種說法大部分情況是對的。然而,使用有狀態 Tokens 與傳統的 Session cookies 基本上是等效的... 但卻缺乏生產環境的大量驗證。
現存的 Session 實現(例如適用於 Express 的 express-session)已經被用於生產環境很多很多年,它們的安全性也經過了大量的改良。倘若使用 JWT 作為 Session cookies 的臨時替代品,你將無法享受到這些好處,並且必須不斷改進自己的實現(在此過程中很容易引入漏洞),或使用第三方的實現,儘管還沒有在真實世界裡大量應用。
譯者注:實際上,Laravel Passport 便是使用類似「有狀態 JWT」的方式來儲存 OAuth Access Token。幸運的是,Passport 已經有不少實際應用,且不完全依賴於 JWT。
結論
無狀態 JWT Tokens 無法被單獨地銷燬或更新,取決於你如何儲存,可能還會導致長度問題、安全隱患。有狀態 JWT Tokens 在功能方面與 Session cookies 無異,但缺乏生產環境的驗證、經過大量 Review 的實現,以及良好的客戶端支援。
除非,你工作在像 BAT 那樣規模的公司,否則沒什麼使用 JWT 作為 Session 機制的理由。還是直接用 Session 吧。
所以... JWT 適合做什麼?
在本文之初,我就提到 JWT 雖然不適合作為 Session 機制,但在其它方面的確有它的用武之地。該主張依舊成立,JWT 特別有效的使用例子通常是作為一次性的授權令牌。
引用 JSON Web Token specification:
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. [...] enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
在此上下文中,「Claim」可能是一條「命令」,一次性的認證,或是基本上能夠用以下句子描述的任何情況:
你好,伺服器 B,伺服器 A 告訴我我可以 < ...Claim... >,這是我的證據:< ...金鑰... >。
舉個例子,你有個檔案服務,使用者必須認證後才能下載檔案,但檔案本身儲存在一臺完全分離且無狀態的「下載伺服器」內。在這種情況下,你可能想要「應用伺服器(伺服器 A)」頒發一次性的「下載 Tokens」,使用者能夠使用它去「下載伺服器(伺服器 B)」獲取需要的檔案。
以這種方式使用 JWT,具備幾個明確的特性:
- Tokens 生命期較短。它們只需在幾分鐘內可用,讓客戶端能夠開始下載。
- Tokens 僅單次使用。應用伺服器應當在每次下載時頒發新的 Token。所以任何 Token 只用於一次請求就會被拋棄,不存在任何持久化的狀態。
- 應用伺服器依舊使用 Sessions。僅僅下載伺服器使用 Tokens 來授權每次下載,因為它不需要任何持久化狀態。
正如以上你所看到的,結合 Sessions 和 JWT Tokens 有理有據。它們分別擁有各自的目的,有時候你需要兩者一起使用。只是不要把 JWT 用作 持久的、長期的 資料就好。