一文帶你搞懂 JWT 常見概念 & 優缺點

JWT 基本概念詳解這篇文章中,我介紹了:

  • 什麼是 JWT?
  • JWT 由哪些部分組成?
  • 如何基於 JWT 進行身份驗證?
  • JWT 如何防止 Token 被篡改?
  • 如何加強 JWT 的安全性?

這篇文章,我們一起探討一下 JWT 身份認證的優缺點以及常見問題的解決辦法。

JWT 的優勢

相比於 Session 認證的方式來說,使用 JWT 進行身份認證主要有下面 4 個優勢。

無狀態

JWT 自身包含了身份驗證所需要的所有信息,因此,我們的服務器不需要存儲 Session 信息。這顯然增加了系統的可用性和伸縮性,大大減輕了服務端的壓力。

不過,也正是由於 JWT 的無狀態,也導致了它最大的缺點:不可控!

就比如說,我們想要在 JWT 有效期內廢棄一個 JWT 或者更改它的權限的話,並不會立即生效,通常需要等到有效期過後纔可以。再比如說,當用戶 Logout 的話,JWT 也還有效。除非,我們在後端增加額外的處理邏輯比如將失效的 JWT 存儲起來,後端先驗證 JWT 是否有效再進行處理。具體的解決辦法,我們會在後面的內容中詳細介紹到,這裏只是簡單提一下。

有效避免了 CSRF 攻擊

CSRF(Cross Site Request Forgery) 一般被翻譯爲 跨站請求僞造,屬於網絡攻擊領域範圍。相比於 SQL 腳本注入、XSS 等安全攻擊方式,CSRF 的知名度並沒有它們高。但是,它的確是我們開發系統時必須要考慮的安全隱患。就連業內技術標杆 Google 的產品 Gmail 也曾在 2007 年的時候爆出過 CSRF 漏洞,這給 Gmail 的用戶造成了很大的損失。

那麼究竟什麼是跨站請求僞造呢? 簡單來說就是用你的身份去做一些不好的事情(發送一些對你不友好的請求比如惡意轉賬)。

舉個簡單的例子:小壯登錄了某網上銀行,他來到了網上銀行的帖子區,看到一個帖子下面有一個鏈接寫着“科學理財,年盈利率過萬”,小壯好奇的點開了這個鏈接,結果發現自己的賬戶少了 10000 元。這是這麼回事呢?原來黑客在鏈接中藏了一個請求,這個請求直接利用小壯的身份給銀行發送了一個轉賬請求,也就是通過你的 Cookie 向銀行發出請求。

<a src="http://www.mybank.com/Transfer?bankId=11&money=10000">科學理財,年盈利率過萬</a>

CSRF 攻擊需要依賴 Cookie ,Session 認證中 Cookie 中的 SessionID 是由瀏覽器發送到服務端的,只要發出請求,Cookie 就會被攜帶。藉助這個特性,即使黑客無法獲取你的 SessionID,只要讓你誤點攻擊鏈接,就可以達到攻擊效果。

另外,並不是必須點擊鏈接纔可以達到攻擊效果,很多時候,只要你打開了某個頁面,CSRF 攻擊就會發生。

<img src="http://www.mybank.com/Transfer?bankId=11&money=10000" />

那爲什麼 JWT 不會存在這種問題呢?

一般情況下我們使用 JWT 的話,在我們登錄成功獲得 JWT 之後,一般會選擇存放在 localStorage 中。前端的每一個請求後續都會附帶上這個 JWT,整個過程壓根不會涉及到 Cookie。因此,即使你點擊了非法鏈接發送了請求到服務端,這個非法請求也是不會攜帶 JWT 的,所以這個請求將是非法的。

總結來說就一句話:使用 JWT 進行身份驗證不需要依賴 Cookie ,因此可以避免 CSRF 攻擊。

不過,這樣也會存在 XSS 攻擊的風險。爲了避免 XSS 攻擊,你可以選擇將 JWT 存儲在標記爲httpOnly 的 Cookie 中。但是,這樣又導致了你必須自己提供 CSRF 保護,因此,實際項目中我們通常也不會這麼做。

常見的避免 XSS 攻擊的方式是過濾掉請求中存在 XSS 攻擊風險的可疑字符串。

在 Spring 項目中,我們一般是通過創建 XSS 過濾器來實現的。

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XSSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException, ServletException {
        XSSRequestWrapper wrappedRequest =
          new XSSRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrappedRequest, response);
    }

    // other methods
}

適合移動端應用

使用 Session 進行身份認證的話,需要保存一份信息在服務器端,而且這種方式會依賴到 Cookie(需要 Cookie 保存 SessionId),所以不適合移動端。

但是,使用 JWT 進行身份認證就不會存在這種問題,因爲只要 JWT 可以被客戶端存儲就能夠使用,而且 JWT 還可以跨語言使用。

單點登錄友好

使用 Session 進行身份認證的話,實現單點登錄,需要我們把用戶的 Session 信息保存在一臺電腦上,並且還會遇到常見的 Cookie 跨域的問題。但是,使用 JWT 進行認證的話, JWT 被保存在客戶端,不會存在這些問題。

JWT 身份認證常見問題及解決辦法

註銷登錄等場景下 JWT 還有效

與之類似的具體相關場景有:

  • 退出登錄;
  • 修改密碼;
  • 服務端修改了某個用戶具有的權限或者角色;
  • 用戶的帳戶被封禁/刪除;
  • 用戶被服務端強制註銷;
  • 用戶被踢下線;
  • ......

這個問題不存在於 Session 認證方式中,因爲在 Session 認證方式中,遇到這種情況的話服務端刪除對應的 Session 記錄即可。但是,使用 JWT 認證的方式就不好解決了。我們也說過了,JWT 一旦派發出去,如果後端不增加其他邏輯的話,它在失效之前都是有效的。

那我們如何解決這個問題呢?查閱了很多資料,我簡單總結了下面 4 種方案:

1、將 JWT 存入內存數據庫

將 JWT 存入 DB 中,Redis 內存數據庫在這裏是不錯的選擇。如果需要讓某個 JWT 失效就直接從 Redis 中刪除這個 JWT 即可。但是,這樣會導致每次使用 JWT 發送請求都要先從 DB 中查詢 JWT 是否存在的步驟,而且違背了 JWT 的無狀態原則。

2、黑名單機制

和上面的方式類似,使用內存數據庫比如 Redis 維護一個黑名單,如果想讓某個 JWT 失效的話就直接將這個 JWT 加入到 黑名單 即可。然後,每次使用 JWT 進行請求的話都會先判斷這個 JWT 是否存在於黑名單中。

前兩種方案的核心在於將有效的 JWT 存儲起來或者將指定的 JWT 拉入黑名單。

雖然這兩種方案都違背了 JWT 的無狀態原則,但是一般實際項目中我們通常還是會使用這兩種方案。

3、修改密鑰 (Secret) :

我們爲每個用戶都創建一個專屬密鑰,如果我們想讓某個 JWT 失效,我們直接修改對應用戶的密鑰即可。但是,這樣相比於前兩種引入內存數據庫帶來了危害更大:

  • 如果服務是分佈式的,則每次發出新的 JWT 時都必須在多臺機器同步密鑰。爲此,你需要將密鑰存儲在數據庫或其他外部服務中,這樣和 Session 認證就沒太大區別了。
  • 如果用戶同時在兩個瀏覽器打開系統,或者在手機端也打開了系統,如果它從一個地方將賬號退出,那麼其他地方都要重新進行登錄,這是不可取的。

4、保持令牌的有效期限短並經常輪換

很簡單的一種方式。但是,會導致用戶登錄狀態不會被持久記錄,而且需要用戶經常登錄。

另外,對於修改密碼後 JWT 還有效問題的解決還是比較容易的。說一種我覺得比較好的方式:使用用戶的密碼的哈希值對 JWT 進行簽名。因此,如果密碼更改,則任何先前的令牌將自動無法驗證。

JWT 的續簽問題

JWT 有效期一般都建議設置的不太長,那麼 JWT 過期後如何認證,如何實現動態刷新 JWT,避免用戶經常需要重新登錄?

我們先來看看在 Session 認證中一般的做法:假如 Session 的有效期 30 分鐘,如果 30 分鐘內用戶有訪問,就把 Session 有效期延長 30 分鐘。

JWT 認證的話,我們應該如何解決續簽問題呢?查閱了很多資料,我簡單總結了下面 4 種方案:

1、類似於 Session 認證中的做法

這種方案滿足於大部分場景。假設服務端給的 JWT 有效期設置爲 30 分鐘,服務端每次進行校驗時,如果發現 JWT 的有效期馬上快過期了,服務端就重新生成 JWT 給客戶端。客戶端每次請求都檢查新舊 JWT,如果不一致,則更新本地的 JWT。這種做法的問題是僅僅在快過期的時候請求才會更新 JWT ,對客戶端不是很友好。

2、每次請求都返回新 JWT

這種方案的的思路很簡單,但是,開銷會比較大,尤其是在服務端要存儲維護 JWT 的情況下。

3、JWT 有效期設置到半夜

這種方案是一種折衷的方案,保證了大部分用戶白天可以正常登錄,適用於對安全性要求不高的系統。

4、用戶登錄返回兩個 JWT

第一個是 accessJWT ,它的過期時間 JWT 本身的過期時間比如半個小時,另外一個是 refreshJWT 它的過期時間更長一點比如爲 1 天。客戶端登錄後,將 accessJWT 和 refreshJWT 保存在本地,每次訪問將 accessJWT 傳給服務端。服務端校驗 accessJWT 的有效性,如果過期的話,就將 refreshJWT 傳給服務端。如果有效,服務端就生成新的 accessJWT 給客戶端。否則,客戶端就重新登錄即可。

這種方案的不足是:

  • 需要客戶端來配合;
  • 用戶註銷的時候需要同時保證兩個 JWT 都無效;
  • 重新請求獲取 JWT 的過程中會有短暫 JWT 不可用的情況(可以通過在客戶端設置定時器,當 accessJWT 快過期的時候,提前去通過 refreshJWT 獲取新的 accessJWT)。

總結

JWT 其中一個很重要的優勢是無狀態,但實際上,我們想要在實際項目中合理使用 JWT 的話,也還是需要保存 JWT 信息。

JWT 也不是銀彈,也有很多缺陷,具體是選擇 JWT 還是 Session 方案還是要看項目的具體需求。萬萬不可尬吹 JWT,而看不起其他身份認證方案。

另外,不用 JWT 直接使用普通的 Token(隨機生成,不包含具體的信息) 結合 Redis 來做身份認證也是可以的。我在 「優質開源項目推薦」第 8 期推薦過的 Sa-Token 這個項目是一個比較完善的 基於 JWT 的身份認證解決方案,支持自動續簽、踢人下線、賬號封禁、同端互斥登錄等功能,感興趣的朋友可以看看。

參考

後記

專注 Java 原創乾貨分享,大三開源 JavaGuide (「Java學習+面試指南」一份涵蓋大部分 Java 程序員所需要掌握的核心知識。準備 Java 面試,首選 JavaGuide!),目前已經 120k+ Star。

原創不易,歡迎點贊分享,歡迎關注我在掘金的賬號,我會持續分享原創乾貨!加油,衝!

如果本文對你有幫助的話,歡迎點贊分享,這對我繼續分享&創作優質文章非常重要。感謝 🙏🏻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章