蝦皮二面后續(xù):JWT 身份認(rèn)證優(yōu)缺點
《Java 面試指北》來啦!這是一份教你如何更高效地準(zhǔn)備面試的小冊,涵蓋常見八股文(系統(tǒng)設(shè)計、常見框架、分布式、高并發(fā) ......)、優(yōu)質(zhì)面經(jīng)等內(nèi)容。
JavaGuide 在線閱讀網(wǎng)站:https://javaguide.cn/
你好,我是 Guide。在 JWT 基本概念詳解這篇文章中,我介紹了:
什么是 JWT? JWT 由哪些部分組成? 如何基于 JWT 進行身份驗證? JWT 如何防止 Token 被篡改? 如何加強 JWT 的安全性?
這篇文章,我們一起探討一下 JWT 身份認(rèn)證的優(yōu)缺點以及常見問題的解決辦法。
JWT 的優(yōu)勢
相比于 Session 認(rèn)證的方式來說,使用 JWT 進行身份認(rèn)證主要有下面 4 個優(yōu)勢。
無狀態(tài)
JWT 自身包含了身份驗證所需要的所有信息,因此,我們的服務(wù)器不需要存儲 Session 信息。這顯然增加了系統(tǒng)的可用性和伸縮性,大大減輕了服務(wù)端的壓力。
不過,也正是由于 JWT 的無狀態(tài),也導(dǎo)致了它最大的缺點:不可控!
就比如說,我們想要在 JWT 有效期內(nèi)廢棄一個 JWT 或者更改它的權(quán)限的話,并不會立即生效,通常需要等到有效期過后才可以。再比如說,當(dāng)用戶 Logout 的話,JWT 也還有效。除非,我們在后端增加額外的處理邏輯比如將失效的 JWT 存儲起來,后端先驗證 JWT 是否有效再進行處理。具體的解決辦法,我們會在后面的內(nèi)容中詳細(xì)介紹到,這里只是簡單提一下。
有效避免了 CSRF 攻擊
CSRF(Cross Site Request Forgery) 一般被翻譯為 跨站請求偽造,屬于網(wǎng)絡(luò)攻擊領(lǐng)域范圍。相比于 SQL 腳本注入、XSS 等安全攻擊方式,CSRF 的知名度并沒有它們高。但是,它的確是我們開發(fā)系統(tǒng)時必須要考慮的安全隱患。就連業(yè)內(nèi)技術(shù)標(biāo)桿 Google 的產(chǎn)品 Gmail 也曾在 2007 年的時候爆出過 CSRF 漏洞,這給 Gmail 的用戶造成了很大的損失。
那么究竟什么是跨站請求偽造呢? 簡單來說就是用你的身份去做一些不好的事情(發(fā)送一些對你不友好的請求比如惡意轉(zhuǎn)賬)。
舉個簡單的例子:小壯登錄了某網(wǎng)上銀行,他來到了網(wǎng)上銀行的帖子區(qū),看到一個帖子下面有一個鏈接寫著“科學(xué)理財,年盈利率過萬”,小壯好奇的點開了這個鏈接,結(jié)果發(fā)現(xiàn)自己的賬戶少了 10000 元。這是這么回事呢?原來黑客在鏈接中藏了一個請求,這個請求直接利用小壯的身份給銀行發(fā)送了一個轉(zhuǎn)賬請求,也就是通過你的 Cookie 向銀行發(fā)出請求。
<a src="http://www.mybank.com/Transfer?bankId=11&money=10000">科學(xué)理財,年盈利率過萬</a>
CSRF 攻擊需要依賴 Cookie ,Session 認(rèn)證中 Cookie 中的 SessionID 是由瀏覽器發(fā)送到服務(wù)端的,只要發(fā)出請求,Cookie 就會被攜帶。借助這個特性,即使黑客無法獲取你的 SessionID,只要讓你誤點攻擊鏈接,就可以達(dá)到攻擊效果。
另外,并不是必須點擊鏈接才可以達(dá)到攻擊效果,很多時候,只要你打開了某個頁面,CSRF 攻擊就會發(fā)生。
<img src="http://www.mybank.com/Transfer?bankId=11&money=10000" />
那為什么 JWT 不會存在這種問題呢?
一般情況下我們使用 JWT 的話,在我們登錄成功獲得 JWT 之后,一般會選擇存放在 localStorage 中。前端的每一個請求后續(xù)都會附帶上這個 JWT,整個過程壓根不會涉及到 Cookie。因此,即使你點擊了非法鏈接發(fā)送了請求到服務(wù)端,這個非法請求也是不會攜帶 JWT 的,所以這個請求將是非法的。
總結(jié)來說就一句話:使用 JWT 進行身份驗證不需要依賴 Cookie ,因此可以避免 CSRF 攻擊。
不過,這樣也會存在 XSS 攻擊的風(fēng)險。為了避免 XSS 攻擊,你可以選擇將 JWT 存儲在標(biāo)記為httpOnly 的 Cookie 中。但是,這樣又導(dǎo)致了你必須自己提供 CSRF 保護,因此,實際項目中我們通常也不會這么做。
常見的避免 XSS 攻擊的方式是過濾掉請求中存在 XSS 攻擊風(fēng)險的可疑字符串。
在 Spring 項目中,我們一般是通過創(chuàng)建 XSS 過濾器來實現(xiàn)的。
@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
}
適合移動端應(yīng)用
使用 Session 進行身份認(rèn)證的話,需要保存一份信息在服務(wù)器端,而且這種方式會依賴到 Cookie(需要 Cookie 保存 SessionId),所以不適合移動端。
但是,使用 JWT 進行身份認(rèn)證就不會存在這種問題,因為只要 JWT 可以被客戶端存儲就能夠使用,而且 JWT 還可以跨語言使用。
單點登錄友好
使用 Session 進行身份認(rèn)證的話,實現(xiàn)單點登錄,需要我們把用戶的 Session 信息保存在一臺電腦上,并且還會遇到常見的 Cookie 跨域的問題。但是,使用 JWT 進行認(rèn)證的話, JWT 被保存在客戶端,不會存在這些問題。
JWT 身份認(rèn)證常見問題及解決辦法
注銷登錄等場景下 JWT 還有效
與之類似的具體相關(guān)場景有:
退出登錄; 修改密碼; 服務(wù)端修改了某個用戶具有的權(quán)限或者角色; 用戶的帳戶被封禁/刪除; 用戶被服務(wù)端強制注銷; 用戶被踢下線; ......
這個問題不存在于 Session 認(rèn)證方式中,因為在 Session 認(rèn)證方式中,遇到這種情況的話服務(wù)端刪除對應(yīng)的 Session 記錄即可。但是,使用 JWT 認(rèn)證的方式就不好解決了。我們也說過了,JWT 一旦派發(fā)出去,如果后端不增加其他邏輯的話,它在失效之前都是有效的。
那我們?nèi)绾谓鉀Q這個問題呢?查閱了很多資料,我簡單總結(jié)了下面 4 種方案:
1、將 JWT 存入內(nèi)存數(shù)據(jù)庫
將 JWT 存入 DB 中,Redis 內(nèi)存數(shù)據(jù)庫在這里是不錯的選擇。如果需要讓某個 JWT 失效就直接從 Redis 中刪除這個 JWT 即可。但是,這樣會導(dǎo)致每次使用 JWT 發(fā)送請求都要先從 DB 中查詢 JWT 是否存在的步驟,而且違背了 JWT 的無狀態(tài)原則。
2、黑名單機制
和上面的方式類似,使用內(nèi)存數(shù)據(jù)庫比如 Redis 維護一個黑名單,如果想讓某個 JWT 失效的話就直接將這個 JWT 加入到 黑名單 即可。然后,每次使用 JWT 進行請求的話都會先判斷這個 JWT 是否存在于黑名單中。
前兩種方案的核心在于將有效的 JWT 存儲起來或者將指定的 JWT 拉入黑名單。
雖然這兩種方案都違背了 JWT 的無狀態(tài)原則,但是一般實際項目中我們通常還是會使用這兩種方案。
3、修改密鑰 (Secret) :
我們?yōu)槊總€用戶都創(chuàng)建一個專屬密鑰,如果我們想讓某個 JWT 失效,我們直接修改對應(yīng)用戶的密鑰即可。但是,這樣相比于前兩種引入內(nèi)存數(shù)據(jù)庫帶來了危害更大:
如果服務(wù)是分布式的,則每次發(fā)出新的 JWT 時都必須在多臺機器同步密鑰。為此,你需要將密鑰存儲在數(shù)據(jù)庫或其他外部服務(wù)中,這樣和 Session 認(rèn)證就沒太大區(qū)別了。 如果用戶同時在兩個瀏覽器打開系統(tǒng),或者在手機端也打開了系統(tǒng),如果它從一個地方將賬號退出,那么其他地方都要重新進行登錄,這是不可取的。
4、保持令牌的有效期限短并經(jīng)常輪換
很簡單的一種方式。但是,會導(dǎo)致用戶登錄狀態(tài)不會被持久記錄,而且需要用戶經(jīng)常登錄。
另外,對于修改密碼后 JWT 還有效問題的解決還是比較容易的。說一種我覺得比較好的方式:使用用戶的密碼的哈希值對 JWT 進行簽名。因此,如果密碼更改,則任何先前的令牌將自動無法驗證。
JWT 的續(xù)簽問題
JWT 有效期一般都建議設(shè)置的不太長,那么 JWT 過期后如何認(rèn)證,如何實現(xiàn)動態(tài)刷新 JWT,避免用戶經(jīng)常需要重新登錄?
我們先來看看在 Session 認(rèn)證中一般的做法:假如 Session 的有效期 30 分鐘,如果 30 分鐘內(nèi)用戶有訪問,就把 Session 有效期延長 30 分鐘。
JWT 認(rèn)證的話,我們應(yīng)該如何解決續(xù)簽問題呢?查閱了很多資料,我簡單總結(jié)了下面 4 種方案:
1、類似于 Session 認(rèn)證中的做法
這種方案滿足于大部分場景。假設(shè)服務(wù)端給的 JWT 有效期設(shè)置為 30 分鐘,服務(wù)端每次進行校驗時,如果發(fā)現(xiàn) JWT 的有效期馬上快過期了,服務(wù)端就重新生成 JWT 給客戶端??蛻舳嗣看握埱蠖紮z查新舊 JWT,如果不一致,則更新本地的 JWT。這種做法的問題是僅僅在快過期的時候請求才會更新 JWT ,對客戶端不是很友好。
2、每次請求都返回新 JWT
這種方案的的思路很簡單,但是,開銷會比較大,尤其是在服務(wù)端要存儲維護 JWT 的情況下。
3、JWT 有效期設(shè)置到半夜
這種方案是一種折衷的方案,保證了大部分用戶白天可以正常登錄,適用于對安全性要求不高的系統(tǒng)。
4、用戶登錄返回兩個 JWT
第一個是 accessJWT ,它的過期時間 JWT 本身的過期時間比如半個小時,另外一個是 refreshJWT 它的過期時間更長一點比如為 1 天。客戶端登錄后,將 accessJWT 和 refreshJWT 保存在本地,每次訪問將 accessJWT 傳給服務(wù)端。服務(wù)端校驗 accessJWT 的有效性,如果過期的話,就將 refreshJWT 傳給服務(wù)端。如果有效,服務(wù)端就生成新的 accessJWT 給客戶端。否則,客戶端就重新登錄即可。
這種方案的不足是:
需要客戶端來配合; 用戶注銷的時候需要同時保證兩個 JWT 都無效; 重新請求獲取 JWT 的過程中會有短暫 JWT 不可用的情況(可以通過在客戶端設(shè)置定時器,當(dāng) accessJWT 快過期的時候,提前去通過 refreshJWT 獲取新的 accessJWT)。
總結(jié)
JWT 其中一個很重要的優(yōu)勢是無狀態(tài),但實際上,我們想要在實際項目中合理使用 JWT 的話,也還是需要保存 JWT 信息。
JWT 也不是銀彈,也有很多缺陷,具體是選擇 JWT 還是 Session 方案還是要看項目的具體需求。萬萬不可尬吹 JWT,而看不起其他身份認(rèn)證方案。
另外,不用 JWT 直接使用普通的 Token(隨機生成,不包含具體的信息) 結(jié)合 Redis 來做身份認(rèn)證也是可以的。我在 「優(yōu)質(zhì)開源項目推薦」 的第 8 期推薦過的 Sa-Token 這個項目是一個比較完善的 基于 JWT 的身份認(rèn)證解決方案,支持自動續(xù)簽、踢人下線、賬號封禁、同端互斥登錄等功能,感興趣的朋友可以看看。

參考
JWT 超詳細(xì)分析:https://learnku.com/articles/17883 How to log out when using JWT:https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6 CSRF protection with JSON Web JWTs:https://medium.com/@agungsantoso/csrf-protection-with-json-web-JWTs-83e0f2fcbcc Invalidating JSON Web JWTs:https://stackoverflow.com/questions/21978658/invalidating-json-web-JWTs
·········· END ··············
歡迎加入我的知識星球獲取更多面試干貨,《Java 面試指北》持續(xù)更新完善中!

近期文章精選 :
走近作者 :
如果本文對你有幫助的話,歡迎點贊&在看&分享,這對我繼續(xù)分享&創(chuàng)作優(yōu)質(zhì)文章非常重要。感謝????
