SpringBootSecurity學習(13)前後端分離版之JWT
JWT 使用
前面簡單介紹了把預設的頁面登入改為前後端分離的介面非同步登入的方法,可以幫我們實現基本的前後端分離登入功能。但是這種基本的登入和前面的頁面登入還有一個一樣的地方,就是使用session和cookie來維護登入狀態,這種方法的問題在於,擴充套件性不好。單機當然沒有問題,如果是伺服器叢集,或者是跨域的服務導向架構,就要求 session 資料共享,每臺伺服器都能夠讀取 session。
一種解決方案是 session 資料持久化,寫入redis或別的持久層。各種服務收到請求後,都向持久層請求資料。這種方案的優點是架構清晰,缺點是工程量比較大。另外,持久層萬一掛了,就會單點失敗。
另一種方案是伺服器索性不儲存 session 資料了,所有資料都儲存在客戶端,每次請求都發回伺服器。JWT 就是這種方案的一個代表。關於JWT的理論知識,建議參考 阮一峰 大神寫的教程 :JSON Web Token 入門教程,這是我認為可能是寫的最清晰的一個,下面的jwt的實現也是根據此教程來實現。
具體的理論知識可以參考教程,這裡簡單說下流程,使用者登入成功後,在header中返回使用者一個token資訊,這個資訊裡面包含了加密的使用者資訊和數字簽名,最重要的還有過期時間,客戶端接到後,每次訪問介面header中都帶著這個token,服務端驗證成功後就表示處於登入狀態,過期後再從新獲取即可。
具體的token內容包含了頭部(加密資訊),載體(使用者資訊),簽名(簽名兩個部分的前面)三大塊,三大塊之間用英文句號(也就是 ".")連線起來,組成一個完整的token資訊
流程設計
根據前面的理論知識,我們來設計一下如何使用jwt。首先我們使用jwt,就可以不再使用session和cookie,所以第一步就是:
- 在security配置檔案中配置session為無狀態。
然後考慮構建jwt訊息體,有三個部分,第一個部分就是頭部,內容是加密型別:
上面程式碼中,alg屬性表示簽名的演算法(algorithm),預設是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的型別(type),JWT 令牌統一寫為JWT,最後,將上面的 JSON 物件使用 Base64URL 演算法轉成字串,作為第一部分。所以第二步就是:
- 在security配置檔案中配置session為無狀態。
- 確定header資訊格式
下一步確定第二部分,訊息載體(Payload),這也是一個json物件,用來存放實際需要傳遞的資料。JWT 規定了7個官方欄位,供選用:
當然除了這些還可以加一些其它內容,比如使用者資訊,這個 JSON 物件也要使用 Base64URL 演算法轉成字串,所以第三步和第四步就是:
- 在security配置檔案中配置session為無狀態。
- 確定header資訊格式
- 確定訊息體
- 使用 HMAC SHA256 演算法 對header和訊息體進行簽名作為第三部分
現在token的訊息基本組合完成了,使用者登入成功和客戶端訪問介面,都要把token放在header裡面,名字是 Authorization 。所以最後一步就是,客戶端正常訪問非登入等介面時,驗證token的合法性,所以,總體設計流程如下:
- 在security配置檔案中配置session為無狀態。
- 確定header資訊格式
- 確定訊息體
- 使用 HMAC SHA256 演算法 對header和訊息體進行簽名作為第三部分
- 新增過濾器,驗證token合法性
修改配置類
上面的流程設計完了,下面我們按照流程修改專案,首先修改security配置類:
配置完後,啟動專案,訪問登入,登入成功後可以看到,沒有任何cookie儲存下來。
定義JWT工具類
首先來定義幾個常量:
然後定義Base64URL 演算法編碼和解碼方法:
然後定義HmacSHA256 加密演算法和獲取簽名的方法:
最後來設計一個簡單驗證token的方法:
這樣jwt工具類就設計好了,目前這幾個方法足夠操作token內容。
定義JWT訊息物件
下面來定義jwt的內容,其實內容很簡單,就三個部分,因此,定義三個欄位即可:
來看一下構造方法,
這個構造方法很便捷,使用它建立物件以後,jwt的三個部分基本都完成了,header部分和payload部分都編碼了,簽名也完成了,因此下面重寫toString方法直接可以生成token:
從這裡可以看出,token整體預設是不加密,但也是可以加密的。生成原始 Token 以後,可以用金鑰再加密一次。因此不要把密碼等重要資訊放入token。
修改登入成功處理器
使用者登入成功後,不再把session發給使用者,而是把jwt傳送給使用者,因此修改登入成功處理器如下:
注意上面手動把使用者的密碼資訊設定為null。這裡為了方便,直接使用fastjson組合物件。
修改實體類
帶著token訪問介面的時候,需要把token轉回登入使用者物件,因此我們的使用者實體類和token中帶的欄位名字一致,來修改一下,先看角色實體類:
再看使用者實體類:
可以看到,基本的原則就是修改的名字和父類的必要欄位名字一致就行,這也是建議的欄位名字。
編寫token驗證過濾器
我們把security的session改為無狀態後,雖然不再傳遞session,但是security的過濾器並沒有失效,因此造成的效果就是登入成功後,訪問介面顯示未登入。現在我們使用token就要在登入前加一個驗證token的過濾器,驗證通過後直接把資訊放到SecurityContextHolder中。這樣每次登入靠驗證token來判斷是否登入,不再靠session。來看這個過濾器:
這個過濾器很簡單,繼承了 GenericFilterBean 類,直接獲取token,判斷token不為空,驗證token,並從token的payload中取出使用者資訊,放入SecurityContextHolder中,驗證失敗或者token過期直接返回token錯誤。邏輯很簡單。
最後在security類中,把這個過濾器配置到前面:
這樣我們自定義的jwt流程就完成了。可以在postman中測試一下,首先是登入:
登入成功後,可以看到header中放著token的資訊,然後使用token放入另一個介面的header中訪問介面,可以看到訪問成功:
有興趣的可以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