SpringBootSecurity學習(13)前後端分離版之JWT

  • 2019 年 10 月 4 日
  • 筆記

JWT 使用

前面簡單介紹了把默認的頁面登錄改為前後端分離的介面非同步登錄的方法,可以幫我們實現基本的前後端分離登錄功能。但是這種基本的登錄和前面的頁面登錄還有一個一樣的地方,就是使用session和cookie來維護登錄狀態,這種方法的問題在於,擴展性不好。單機當然沒有問題,如果是伺服器集群,或者是跨域的服務導向架構,就要求 session 數據共享,每台伺服器都能夠讀取 session。

一種解決方案是 session 數據持久化,寫入redis或別的持久層。各種服務收到請求後,都向持久層請求數據。這種方案的優點是架構清晰,缺點是工程量比較大。另外,持久層萬一掛了,就會單點失敗。

另一種方案是伺服器索性不保存 session 數據了,所有數據都保存在客戶端,每次請求都發回伺服器。JWT 就是這種方案的一個代表。關於JWT的理論知識,建議參考 阮一峰 大神寫的教程 :JSON Web Token 入門教程,這是我認為可能是寫的最清晰的一個,下面的jwt的實現也是根據此教程來實現。

具體的理論知識可以參考教程,這裡簡單說下流程,用戶登錄成功後,在header中返回用戶一個token資訊,這個資訊裡面包含了加密的用戶資訊和數字簽名,最重要的還有過期時間,客戶端接到後,每次訪問介面header中都帶著這個token,服務端驗證成功後就表示處於登錄狀態,過期後再從新獲取即可。

具體的token內容包含了頭部(加密資訊),載體(用戶資訊),簽名(簽名兩個部分的前面)三大塊,三大塊之間用英文句號(也就是 ".")連接起來,組成一個完整的token資訊

流程設計

根據前面的理論知識,我們來設計一下如何使用jwt。首先我們使用jwt,就可以不再使用session和cookie,所以第一步就是:

  1. 在security配置文件中配置session為無狀態。

然後考慮構建jwt消息體,有三個部分,第一個部分就是頭部,內容是加密類型:

file

上面程式碼中,alg屬性表示簽名的演算法(algorithm),默認是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫為JWT,最後,將上面的 JSON 對象使用 Base64URL 演算法轉成字元串,作為第一部分。所以第二步就是:

  1. 在security配置文件中配置session為無狀態。
  2. 確定header資訊格式

下一步確定第二部分,消息載體(Payload),這也是一個json對象,用來存放實際需要傳遞的數據。JWT 規定了7個官方欄位,供選用:

file

當然除了這些還可以加一些其它內容,比如用戶資訊,這個 JSON 對象也要使用 Base64URL 演算法轉成字元串,所以第三步和第四步就是:

  1. 在security配置文件中配置session為無狀態。
  2. 確定header資訊格式
  3. 確定消息體
  4. 使用 HMAC SHA256 演算法 對header和消息體進行簽名作為第三部分

現在token的消息基本組合完成了,用戶登錄成功和客戶端訪問介面,都要把token放在header裡面,名字是 Authorization 。所以最後一步就是,客戶端正常訪問非登錄等介面時,驗證token的合法性,所以,總體設計流程如下:

  1. 在security配置文件中配置session為無狀態。
  2. 確定header資訊格式
  3. 確定消息體
  4. 使用 HMAC SHA256 演算法 對header和消息體進行簽名作為第三部分
  5. 添加過濾器,驗證token合法性

修改配置類

上面的流程設計完了,下面我們按照流程修改項目,首先修改security配置類:

file

配置完後,啟動項目,訪問登錄,登錄成功後可以看到,沒有任何cookie保存下來。

定義JWT工具類

首先來定義幾個常量:

file

然後定義Base64URL 演算法編碼和解碼方法:

file

然後定義HmacSHA256 加密演算法和獲取簽名的方法:

file

最後來設計一個簡單驗證token的方法:

file

這樣jwt工具類就設計好了,目前這幾個方法足夠操作token內容。

定義JWT消息對象

下面來定義jwt的內容,其實內容很簡單,就三個部分,因此,定義三個欄位即可:

file

來看一下構造方法,

file

這個構造方法很便捷,使用它創建對象以後,jwt的三個部分基本都完成了,header部分和payload部分都編碼了,簽名也完成了,因此下面重寫toString方法直接可以生成token:

file

從這裡可以看出,token整體默認是不加密,但也是可以加密的。生成原始 Token 以後,可以用密鑰再加密一次。因此不要把密碼等重要資訊放入token。

修改登錄成功處理器

用戶登錄成功後,不再把session發給用戶,而是把jwt發送給用戶,因此修改登錄成功處理器如下:

file

注意上面手動把用戶的密碼資訊設置為null。這裡為了方便,直接使用fastjson組合對象。

修改實體類

帶著token訪問介面的時候,需要把token轉回登錄用戶對象,因此我們的用戶實體類和token中帶的欄位名字一致,來修改一下,先看角色實體類:

file

再看用戶實體類:

file

可以看到,基本的原則就是修改的名字和父類的必要欄位名字一致就行,這也是建議的欄位名字。

編寫token驗證過濾器

我們把security的session改為無狀態後,雖然不再傳遞session,但是security的過濾器並沒有失效,因此造成的效果就是登錄成功後,訪問介面顯示未登錄。現在我們使用token就要在登錄前加一個驗證token的過濾器,驗證通過後直接把資訊放到SecurityContextHolder中。這樣每次登錄靠驗證token來判斷是否登錄,不再靠session。來看這個過濾器:

file

這個過濾器很簡單,繼承了 GenericFilterBean 類,直接獲取token,判斷token不為空,驗證token,並從token的payload中取出用戶資訊,放入SecurityContextHolder中,驗證失敗或者token過期直接返回token錯誤。邏輯很簡單。

最後在security類中,把這個過濾器配置到前面:

file

這樣我們自定義的jwt流程就完成了。可以在postman中測試一下,首先是登錄:

file

登錄成功後,可以看到header中放著token的資訊,然後使用token放入另一個介面的header中訪問介面,可以看到訪問成功:

file

有興趣的可以debug跟蹤一下流程。

JWT的幾個特點

  • (1)JWT 默認是不加密,但也是可以加密的。生成原始 Token 以後,可以用密鑰再加密一次。

  • (2)JWT 不加密的情況下,不能將秘密數據寫入 JWT。

  • (3)JWT 不僅可以用於認證,也可以用於交換資訊。有效使用 JWT,可以降低伺服器查詢資料庫的次數。

  • (4)JWT 的最大缺點是,由於伺服器不保存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的許可權。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非伺服器部署額外的邏輯。

  • (5)JWT 本身包含了認證資訊,一旦泄露,任何人都可以獲得該令牌的所有許可權。為了減少盜用,JWT 的有效期應該設置得比較短。對於一些比較重要的許可權,使用時應該再次對用戶進行認證。

  • (6)為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。

程式碼地址:https://gitee.com/blueses/spring-boot-security 14