實戰!退出登錄時如何藉助外力使JWT令牌失效?

大家好,我是不才陳某~

今天這篇文章介紹一下如何在修改密碼、修改許可權、註銷等場景下使JWT失效。

文章的目錄如下:

解決方案

JWT最大的一個優勢在於它是無狀態的,自身包含了認證鑒權所需要的所有資訊,伺服器端無需對其存儲,從而給伺服器減少了存儲開銷。

但是無狀態引出的問題也是可想而知的,它無法作廢未過期的JWT。舉例說明註銷場景下,就傳統的cookie/session認證機制,只需要把存在伺服器端的session刪掉就OK了。

但是JWT呢,它是不存在伺服器端的啊,好的那我刪存在客戶端的JWT行了吧。額,社會本就複雜別再欺騙自己了好么,被你在客戶端刪掉的JWT還是可以通過伺服器端認證的。

使用JWT要非常明確的一點:JWT失效的唯一途徑就是等待時間過期。

但是可以藉助外力保存JWT的狀態,這時就有人問了:你這不是打臉嗎?用JWT就因為它的無狀態性,這時候又要保存它的狀態?

其實不然,這不被逼上梁山了嗎?不使用外力保存JWT的狀態,你說如何實現註銷失效?

常用的方案有兩種,白名單和黑名單方式。

1、白名單

白名單的邏輯很簡單:認證通過時,將JWT存入redis中,註銷時,將JWT從redis中移出。這種方式和cookie/session的方式大同小異。

2、黑名單

黑名單的邏輯也非常簡單:註銷時,將JWT放入redis中,並且設置過期時間為JWT的過期時間;請求資源時判斷該JWT是否在redis中,如果存在則拒絕訪問。

白名單和黑名單這兩種方案都比較好實現,但是黑名單帶給伺服器的壓力遠遠小於白名單,畢竟註銷不是經常性操作。

黑名單方式實現

下面以黑名單的方式介紹一下如何在網關層面實現JWT的註銷失效。

究竟向Redis中存儲什麼?

如果直接存儲JWT令牌可行嗎?當然可行,不過JWT令牌可是很長的哦,這樣對記憶體的要求也是挺高的。

熟悉JWT令牌的都知道,JWT令牌中有一個jti欄位,這個欄位可以說是JWT令牌的唯一ID了,如下:

因此可以將這個jti欄位存入redis中,作為唯一令牌標識,這樣一來是不是節省了很多的記憶體?

如何實現呢? 分為兩步:

  1. 網關層的全局過濾器中需要判斷黑名單是否存在當前JWT
  2. 註銷介面中將JWT的jti欄位作為key存放到redis中,且設置了JWT的過期時間

1、網關層解析JWT的jti、過期時間放入請求頭中

在網關的全局過濾器GlobalAuthenticationFilter中直接從令牌中解析出jti和過期時間。

這裡的邏輯分為如下步驟:

  1. 解析JWT令牌的jti和過期時間
  2. 根據jti從redis中查詢是否存在黑名單中,如果存在則直接攔截,否則放行
  3. 將解析的jti和過期時間封裝到JSON中,傳遞給下游微服務

關鍵程式碼如下:

2、下游微服務的過濾器修改

還記得上篇文章:實戰乾貨!Spring Cloud Gateway 整合 OAuth2.0 實現分散式統一認證授權!中微服務的過濾器AuthenticationFilter嗎?

AuthenticationFilter這個過濾器用來解密網關層傳遞的JSON數據,並將其封裝到Request中,這樣在業務方法中便可以隨時獲取到想要的用戶資訊。

這裡我是把JWT相關的資訊同時封裝到了Request中,實體類為JwtInformation,如下:

LoginVal繼承了JwtInformation,如下:

此時AuthenticationFilter這個過濾器修改起來就很簡單了,只需要將jti和過期時間封裝到LoginVal中即可,關鍵程式碼如下:

邏輯很簡單,上圖都有標註。

3、註銷介面實現

之前文章中並沒有提供註銷介面,因為無狀態的JWT根本不需要退出登錄,傻等著過期唄。

當然為了實現註銷登錄,藉助了Redis,那麼註銷介面必不可少了。

邏輯很簡單,直接將退出登錄的JWT令牌的jti設置到Redis中,過期時間設置為JWT過期時間即可。程式碼如下:

OK了,至此已經實現了JWT註銷登錄的功能.......

涉及到的三個模組的改動,分別如下:

名稱 功能
oauth2-cloud-auth-server OAuth2.0認證授權服
oauth2-cloud-gateway 網關服務
oauth2-cloud-auth-common 公共模組

總結

思想很簡單,JWT既然是無狀態的,只能藉助Redis記錄它的狀態,這樣才能達到使其失效的目的。

測試

業務基本完成了,下面走一個流程測試一下,如下:

1、登錄,申請令牌

2、拿著令牌訪問介面

該令牌並沒有註銷,因此可以正常訪問,如下:

3、調用介面註銷登錄

請求如下:

4、拿著註銷的令牌訪問介面

由於令牌已經註銷了,因此肯定訪問不通介面,返回如下: