翻譯自 Mohamad Lawand 2021年3月11日的文章 《Intro to JWT - Step by Step》 [1]
我們今天要講的內容包含:
- JWT 是什么
- 我們應該在什么時候使用它
- JWT 與 Session Id 比較
- JWT 結構
- JWT 簽名
JWT 是什么
JWT (JSON Web Token) 是一個開放標准[3],它定義了一種以緊湊和自包含的方法,用於在雙方之間安全地傳輸編碼為 JSON 對象的信息。
因此,簡單來說,它是 JSON 格式的加密字符串,其中包含敏感信息,它使我們能夠驗證不同服務間的發送者。
應該在什么時候使用 JWT?
- 授權: 這是使用 JWT 最常見的場景。JWT 用於授權而非身份驗證。通過身份驗證,我們驗證用戶名和密碼是否有效,並將用戶登錄到系統中。通過授權,我們可以驗證發送到服務器的請求是否屬於通過身份驗證登錄的用戶,從而可以授予該用戶訪問系統的權限,繼而批准該用戶使用獲得的 token 訪問路由、服務和資源。
- 信息交換: JSON Web Token 是在雙方之間安全地傳輸信息的一種好方法。因為 JWT 可以被簽名(例如,使用公鑰/私鑰對),所以使您能確保發送方是他們所聲稱的那一方。此外,由於簽名是使用 Header 和 Payload 計算的,因此還使您能驗證發送的內容沒有被篡改。
JWT 與 Session Id 比較
小型 Web 應用程序
Session Id 實現
在傳統的 Web 應用程序中,我們使用 Session 來授權用戶,當用戶登錄到應用程序后,我們會為該用戶分配一個唯一的 Session Id。我們將此 Session Id 保存在用戶瀏覽器的安全 cookie 中和服務器的內存中。我們對每個請求都使用相同的會話,以便服務器知道該用戶已通過身份驗證。對於每個請求,cookie 中的 Session Id 都會與服務器內存中的 Session Id 作匹配,以驗證用戶是否被授權。
JWT 實現
在 JWT 實現中,我們使用 JWT 授權用戶,當用戶登錄到應用程序后,就會為每個通過身份驗證的用戶生成一個唯一的 JWT。我們將該 token 保存在瀏覽器的 local storage 或者 cookie 中,而不會在服務器端保存任何內容。對於每個請求,該 token 都會被發送到服務器進行解密和驗證,以核實該用戶是否已授權,不管以何種方式篡改了 token 都會被拒絕。
這種實現對於小型站點來說很好,僅僅因為我們不再存儲 Session Id,從而通過減少服務器的負載,我們已經從 JWT 中看到了一些好處。
高級 Web 應用程序(多個服務器)
如果我們的應用程序越來越受歡迎,需要我們對其進行擴展,會發生什么呢?
Session Id 實現
我們需要有一台連接到負載均衡器的新服務器,以便基於流量和可用性在 Web 服務器之間導航流量。這種實現給我們帶來了一個新的問題,如下所示:
如果用戶 1 登錄到了服務器 1,那么服務器 1 已經將 session 保存在其內存中,當用戶 1 發出另一個請求並且負載均衡器將該請求重定向到了服務器 2,而服務器 2 沒有保存該 session 信息,這時會發生什么情況?
用戶將被認為已退出應用程序並被要求再次登錄,這不是一個好的用戶體驗。通常,我們解決這個問題的方法是引入緩存:
現在,所有的 Session 也將同時保存在緩存中,因此任何一台服務器都可以檢查該 Session 是否存在,並可以利用它來驗證用戶並授予他們對應用程序的訪問權限。
盡管緩存解決了我們的問題,但是在生產環境中,這種解決方案有着昂貴的成本:
- 需要大量的 RAM、CPU、存儲來跟蹤所有這些會話和平穩地處理請求
- 需要維護緩存,以確保沒有幽靈會話或無效會話
- 萬一某台服務器崩潰,所有未與緩存同步的會話都會丟失
- 使用戶無效更復雜
- 托管成本高
JWT 實現
讓我們來看看如何通過 JWT 實現來處理相同的情況。
不同於在 Cookie 中使用 Session Id 與服務器內存中的 Session 作匹配;我們可以使用 JWT 來代替它。此時,當用戶登錄到我們的應用程序時,服務器將不會生成 Session Id 並將其保存在內存中,而是會創建一個 JWT token,並對其進行編碼和序列化,然后使用自己的加密機制對其進行簽名。通過這種方式,服務將知道一旦對它做了變更或篡改,便將其變為無效。由於通過服務器的加密機制對其進行了簽名,所以這是可以被檢驗的。
使用 JWT 可以更容易地管理可伸縮性,因為我們不需要服務器來處理任何會話檢查或緩存檢查。請求可以轉發到負載均衡器為其分配的任一服務器,而無需擔心會話的可用性。萬一某台服務器宕機,所有的 token 將仍然有效,因為所有服務器上的加密機制是一樣的。
JWT 和 Session Id 的區別總結
讓我們來快速總結一下 JWT 和 Session Id 的區別
JWT
- 服務器上不保存任何東西,JWT 存儲於客戶端中
- 由服務器加密和簽名
- token 包含用戶的所有信息
- 所有信息都存儲於 token 本身中
- 易於縮放
Session Id
- Session Id 保存於服務器和客戶端中
- 加密並簽名
- Session Id 是對用戶的引用
- 服務器需要查找用戶並進行必要的檢查
- 難以縮放
JWT 結構
JSON Web Token 由三部分組成,以點(.)分隔,分別是:
- Header(標頭)
- Payload(有效負載)
- Signature(簽名)
因此,JWT 通常如下所示:
xxxxxx.yyyyyyy.zzzzzzzz
這種分隔使從視覺上更容易看出 token 的不同部分。讓我們來分解一下它的不同的部分。
Header
Header 通常由兩部分組成:
- token 的類型,這里是 JWT
- 使用的簽名算法,比如 HMAC、SHA256 或 RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,將此 JSON 以 Base64Url 編碼,形成 JWT 的第一部分。
Payload (Data)
token 的第二部分是有效負載,其中包含 Claims(聲明)。Claims 是有關實體(通常是用戶)和其他數據的聲明。有三種類型的 Claims:registered、public 和 private claims。
- Registered Claims:這是一組非強制性的但建議使用的預定義 Claims,以提供一組有用的、可互操作的 Claims。其中包含:iss (issuer,簽發者)、exp (expiration time,過期時間)、sub (subject,主題)、aud (audience,受眾) 等等。
- Public Claims:這些可以由使用 JWT 的人員隨意定義。但是為了避免沖突,應該在 IANA JSON Web Token Registry 中定義它們,或者將其定義為包含抗沖突命名空間的 URI。
- Private Claims:這些是自定義聲明,是為了在同意使用它們的雙方共享信息而創建的,它們既不是注冊的聲明,也不是公共的聲明。
舉一個有效負載的例子:
{
"sub": "221122112",
"name": "Mohamd Lawand",
"admin": true,
"exp": 15323232,
"iat": 14567766 // 該 token 的簽發時間
}
然后,對有效負載進行 Base64Url 編碼,形成 JSON Web Token 的第二部分。
除非將其加密,否則請不要將機密信息放入 JWT 的 Payload 或 Header 元素中。
簽名
簽名使我們能夠驗證 token 是否有效和沒被篡改。它的工作方式是獲取 token 的前兩部分,將 Header 和 Payload 分別編碼為 Base64,然后將它們用 “.” 連接起來。這樣我們就擁有了與用戶共享的所有數據。
然后,獲取在第一部分(Header)中提供的算法並應用於上面的連接結果。如果前兩部分的哈希結果與 token 的第三部分匹配,則表示此 JWT 是有效的;如果不匹配,則表示此 token 被修改過,是無效的。
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret)
這種方案的唯一威脅是密鑰在服務器以外的任何地方都可用。但是,如果我們保護好密鑰的安全,就沒有什么能損害這一過程。
簽名被用於驗證消息在傳輸過程中沒有被篡改,而且,當 token 是使用私鑰簽名時,它還可以驗證 JWT 發送方的真實身份。
它的工作原理與密碼哈希非常相似——我們將兩部分組合在一起,並且使用特定的算法進行單向哈希,然后我們比較哈希的結果看它們是否有效。
簽名方式
現在,讓我們來看一下對 JWT 進行簽名的方式:
- 一個 secret(使用 HMAC 算法)
- 一個 公鑰/私鑰對(使用 RSA 或 ECDSA)
簽名的 token 可以驗證其中包含的 Claims 的完整性,而加密的 token 則可以向其他方隱藏這些 Claims。
https://dev.to/moe23/intro-to-jwt-mcb Intro to JWT - Step by Step ↩︎
https://jwt.io/ jwt.io ↩︎
https://tools.ietf.org/html/rfc7519 RFC 7519 ↩︎