在 JWT 基本概念詳解這篇文章中,我介紹了:
這篇文章,我們一起探討一下 JWT 身份認證的優缺點以及常見問題的解決辦法。
相比於 Session 認證的方式來說,使用 JWT 進行身份認證主要有下面 4 個優勢。
JWT 自身包含了身份驗證所需要的所有資訊,因此,我們的伺服器不需要儲存 Session 資訊。這顯然增加了系統的可用性和伸縮性,大大減輕了伺服器端的壓力。
不過,也正是由於 JWT 的無狀態,也導致了它最大的缺點:不可控!
就比如說,我們想要在 JWT 有效期內廢棄一個 JWT 或者更改它的許可權的話,並不會立即生效,通常需要等到有效期過後才可以。再比如說,當用戶 Logout 的話,JWT 也還有效。除非,我們在後端增加額外的處理邏輯比如將失效的 JWT 儲存起來,後端先驗證 JWT 是否有效再進行處理。具體的解決辦法,我們會在後面的內容中詳細介紹到,這裡只是簡單提一下。
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 被儲存在使用者端,不會存在這些問題。
與之類似的具體相關場景有:
這個問題不存在於 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 失效,我們直接修改對應使用者的金鑰即可。但是,這樣相比於前兩種引入記憶體資料庫帶來了危害更大:
4、保持令牌的有效期限短並經常輪換
很簡單的一種方式。但是,會導致使用者登入狀態不會被持久記錄,而且需要使用者經常登入。
另外,對於修改密碼後 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 資訊。
JWT 也不是銀彈,也有很多缺陷,具體是選擇 JWT 還是 Session 方案還是要看專案的具體需求。萬萬不可尬吹 JWT,而看不起其他身份認證方案。
另外,不用 JWT 直接使用普通的 Token(隨機生成,不包含具體的資訊) 結合 Redis 來做身份認證也是可以的。我在 「優質開源專案推薦」 的第 8 期推薦過的 Sa-Token 這個專案是一個比較完善的 基於 JWT 的身份認證解決方案,支援自動續簽、踢人下線、賬號封禁、同端互斥登入等功能,感興趣的朋友可以看看。
專注 Java 原創乾貨分享,大三開源 JavaGuide (「Java學習+面試指南」一份涵蓋大部分 Java 程式設計師所需要掌握的核心知識。準備 Java 面試,首選 JavaGuide!),目前已經 120k+ Star。
原創不易,歡迎點贊分享,歡迎關注我在掘金的賬號,我會持續分享原創乾貨!加油,衝!
如果本文對你有幫助的話,歡迎點贊分享,這對我繼續分享&創作優質文章非常重要。感謝