HTTP 是一個無狀態的協議,一次請求結束後,下次在傳送伺服器就不知道這個請求是誰發來的了(同一個 IP 不代表同一個使用者),在 Web 應用中,使用者的認證和鑑權是非常重要的一環,實踐中有多種可用方案,並且各有千秋。
基於 Session 的會話管理
在 Web 應用發展的初期,大部分採用基於 Session 的會話管理方式,邏輯如下。
- 客戶端使用使用者名稱密碼進行認證
- 服務端生成並儲存 Session,將 SessionID 通過 Cookie 返回給客戶端
- 客戶端訪問需要認證的介面時在 Cookie 中攜帶 SessionID
- 服務端通過 SessionID 查詢 Session 並進行鑑權,返回給客戶端需要的資料
基於 Session 的方式存在多種問題。
- 服務端需要儲存 Session,並且由於 Session 需要經常快速查詢,通常儲存在記憶體或記憶體資料庫中,同時線上使用者較多時需要佔用大量的伺服器資源。
- 當需要擴充套件時,建立 Session 的伺服器可能不是驗證 Session 的伺服器,所以還需要將所有 Session 單獨儲存並共享。
- 由於客戶端使用 Cookie 儲存 SessionID,在跨域場景下需要進行相容性處理,同時這種方式也難以防範 CSRF 攻擊。
基於 Token 的會話管理
鑑於基於 Session 的會話管理方式存在上述多個缺點,無狀態的基於 Token 的會話管理方式誕生了,所謂無狀態,就是服務端不再儲存資訊,甚至是不再儲存 Session,邏輯如下。
- 客戶端使用使用者名稱密碼進行認證
- 服務端驗證使用者名稱密碼,通過後生成 Token 返回給客戶端
- 客戶端儲存 Token,訪問需要認證的介面時在 URL 引數或 HTTP Header 中加入 Token
- 服務端通過解碼 Token 進行鑑權,返回給客戶端需要的資料
基於 Token 的會話管理方式有效解決了基於 Session 的會話管理方式帶來的問題。
- 服務端不需要儲存和使用者鑑權有關的資訊,鑑權資訊會被加密到 Token 中,服務端只需要讀取 Token 中包含的鑑權資訊即可
- 避免了共享 Session 導致的不易擴充套件問題
- 不需要依賴 Cookie,有效避免 Cookie 帶來的 CSRF 攻擊問題
- 使用 CORS 可以快速解決跨域問題
JWT 介紹
JWT 是 JSON Web Token 的縮寫,JWT 本身沒有定義任何技術實現,它只是定義了一種基於 Token 的會話管理的規則,涵蓋 Token 需要包含的標準內容和 Token 的生成過程。
一個 JWT Token 長這樣。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDQ1MTE3NDMsImp0aSI6IjYxYmVmNjkyLTE4M2ItNGYxYy1hZjE1LWUwMDM0MTczNzkxOSJ9.CZzB2-JI1oPRFxNMaoFz9-9cKGTYVXkOC2INMoEYNNA
複製程式碼
仔細辨別會發現它由 A.B.C
三部分組成,這三部分依次是頭部(Header)、負載(Payload)、簽名(Signature),頭部和負載以 JSON 形式存在,這就是 JWT 中的 JSON,三部分的內容都分別單獨經過了 Base64 編碼,以 .
拼接成一個 JWT Token。
JWT 的 Header 中儲存了所使用的加密演算法和 Token 型別。
{
"alg": "HS256",
"typ": "JWT"
}
複製程式碼
Payload 是負載,JWT 規範規定了一些欄位,並推薦使用,開發者也可以自己指定欄位和內容,例如下面的內容。
{
username: 'yage',
email: 'sa@simpleapples.com',
role: 'user',
exp: 1544602234
}
複製程式碼
需要注意的是,Payload的內容只經過了 Base64 編碼,對客戶端來說當於明文儲存,所以不要放置敏感資訊。
Signature 部分用來驗證 JWT Token 是否被篡改,所以這部分會使用一個 Secret 將前兩部分加密,邏輯如下。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
複製程式碼
JWT 優勢 & 問題
JWT 擁有基於 Token 的會話管理方式所擁有的一切優勢,不依賴 Cookie,使得其可以防止 CSRF 攻擊,也能在禁用 Cookie 的瀏覽器環境中正常執行。
而 JWT 的最大優勢是服務端不再需要儲存 Session,使得服務端認證鑑權業務可以方便擴充套件,避免儲存 Session 所需要引入的 Redis 等元件,降低了系統架構複雜度。但這也是 JWT 最大的劣勢,由於有效期儲存在 Token 中,JWT Token 一旦簽發,就會在有效期內一直可用,無法在服務端廢止,當使用者進行登出操作,只能依賴客戶端刪除掉本地儲存的 JWT Token,如果需要禁用使用者,單純使用 JWT 就無法做到了。
基於 JWT 的實踐
既然 JWT 依然存在諸多問題,甚至無法滿足一些業務上的需求,但是我們依然可以基於 JWT 在實踐中進行一些改進,來形成一個折中的方案,畢竟,在使用者會話管理場景下,沒有銀彈。
前面講的 Token,都是 Access Token,也就是訪問資源介面時所需要的 Token,還有另外一種 Token,Refresh Token,通常情況下,Refresh Token 的有效期會比較長,而 Access Token 的有效期比較短,當 Access Token 由於過期而失效時,使用 Refresh Token 就可以獲取到新的 Access Token,如果 Refresh Token 也失效了,使用者就只能重新登入了。
在 JWT 的實踐中,引入 Refresh Token,將會話管理流程改進如下。
- 客戶端使用使用者名稱密碼進行認證
- 服務端生成有效時間較短的 Access Token(例如 10 分鐘),和有效時間較長的 Refresh Token(例如 7 天)
- 客戶端訪問需要認證的介面時,攜帶 Access Token
- 如果 Access Token 沒有過期,服務端鑑權後返回給客戶端需要的資料
- 如果攜帶 Access Token 訪問需要認證的介面時鑑權失敗(例如返回 401 錯誤),則客戶端使用 Refresh Token 向重新整理介面申請新的 Access Token
- 如果 Refresh Token 沒有過期,服務端向客戶端下發新的 Access Token
- 客戶端使用新的 Access Token 訪問需要認證的介面
將生成的 Refresh Token 以及過期時間儲存在服務端的資料庫中,由於 Refresh Token 不會在客戶端請求業務介面時驗證,只有在申請新的 Access Token 時才會驗證,所以將 Refresh Token 儲存在資料庫中,不會對業務介面的響應時間造成影響,也不需要像 Session 一樣一直保持在記憶體中以應對大量的請求。
上述的架構,提供了服務端禁用使用者 Token 的方式,當使用者需要登出或禁用使用者時,只需要將服務端的 Refresh Token 禁用或刪除,使用者就會在 Access Token 過期後,由於無法獲取到新的 Access Token 而再也無法訪問需要認證的介面。這樣的方式雖然會有一定的視窗期(取決於 Access Token 的失效時間),但是結合使用者登出時客戶端刪除 Access Token 的操作,基本上可以適應常規情況下對使用者認證鑑權的精度要求。
總結
JWT 的使用,提高了開發者開發使用者認證鑑權功能的效率,降低了系統架構複雜度,避免了大量的資料庫和快取查詢,降低了業務介面的響應延遲。然而 JWT 的這些優點也增加了 Token 管理上的難度,通過引入 Refresh Token,既能繼續使用 JWT 所帶來的優勢,又能使得 Token 管理的精度符合業務的需求。