通過 JWT 配合 Spring Security OAuth2 使用的方式,可以避免 每次請求 都 遠程調度 認證授權服務。 資源服務器 只需要從 授權服務器 驗證壹次,返回 JWT。返回的 JWT 包含了 用戶 的所有信息,包括 權限信息 。
1. 什麽是JWT
JSON Web Token(JWT)是壹種開放的標準(RFC 7519),JWT 定義了壹種 緊湊 且 自包含 的標準,旨在將各個主體的信息包裝為 JSON 對象。 主體信息 是通過 數字簽名 進行 加密 和 驗證 的。經常使用 HMAC 算法或 RSA( 公鑰 / 私鑰 的 非對稱性加密 )算法對 JWT 進行簽名, 安全性很高 。
2. JWT的結構
JWT 的結構由三部分組成:Header(頭)、Payload(有效負荷)和 Signature(簽名)。因此 JWT 通常的格式是 xxxxx.yyyyy.zzzzz。
2.1. Header
Header 通常是由 兩部分 組成:令牌的 類型 (即 JWT)和使用的 算法類型 ,如 HMAC、SHA256 和 RSA。例如:
將 Header 用 Base64 編碼作為 JWT 的 第壹部分 ,不建議在 JWT 的 Header 中放置 敏感信息 。
2.2. Payload
下面是 Payload 部分的壹個示例:
將 Payload 用 Base64 編碼作為 JWT 的 第二部分 ,不建議在 JWT 的 Payload 中放置 敏感信息 。
2.3. Signature
要創建簽名部分,需要利用 秘鑰 對 Base64 編碼後的 Header 和 Payload 進行 加密 ,加密算法的公式如下:
簽名 可以用於驗證 消息 在 傳遞過程 中有沒有被更改。對於使用 私鑰簽名 的 token,它還可以驗證 JWT 的 發送方 是否為它所稱的 發送方 。
3. JWT的工作方式
客戶端 獲取 JWT 後,對於以後的 每次請求 ,都不需要再通過 授權服務 來判斷該請求的 用戶 以及該 用戶的權限 。在微服務系統中,可以利用 JWT 實現 單點登錄 。認證流程圖如下:
4. 案例工程結構
工程原理示意圖如下:
5. 構建auth-service授權服務
UserServiceDetail.java
UserRepository.java
實體類 User 和上壹篇文章的內容壹樣,需要實現 UserDetails 接口,實體類 Role 需要實現 GrantedAuthority 接口。
User.java
Role.java
jks 文件的生成需要使用 Java keytool 工具,保證 Java 環境變量沒問題,輸入命令如下:
其中,-alias 選項為 別名 ,-keyalg 為 加密算法 ,-keypass 和 -storepass 為 密碼選項 ,-keystore 為 jks 的 文件名稱 ,-validity 為配置 jks 文件 過期時間 (單位:天)。
生成的 jks 文件作為 私鑰 ,只允許 授權服務 所持有,用作 加密生成 JWT。把生成的 jks 文件放到 auth-service 模塊的 src/main/resource 目錄下即可。
對於 user-service 這樣的 資源服務 ,需要使用 jks 的 公鑰 對 JWT 進行 解密 。獲取 jks 文件的 公鑰 的命令如下:
這個命令要求安裝 openSSL 下載地址,然後手動把安裝的 openssl.exe 所在目錄配置到 環境變量 。
輸入密碼 fzp123 後,顯示的信息很多,只需要提取 PUBLIC KEY,即如下所示:
新建壹個 public.cert 文件,將上面的 公鑰信息 復制到 public.cert 文件中並保存。並將文件放到 user-service 等 資源服務 的 src/main/resources 目錄下。至此 auth-service 搭建完畢。
maven 在項目編譯時,可能會將 jks 文件 編譯 ,導致 jks 文件 亂碼 ,最後不可用。需要在 pom.xml 文件中添加以下內容:
6. 構建user-service資源服務
註入 JwtTokenStore 類型的 Bean,同時初始化 JWT 轉換器 JwtAccessTokenConverter,設置用於解密 JWT 的 公鑰 。
配置 資源服務 的認證管理,除了 註冊 和 登錄 的接口之外,其他的接口都需要 認證 。
新建壹個配置類 GlobalMethodSecurityConfig,通過 @EnableGlobalMethodSecurity 註解開啟 方法級別 的 安全驗證 。
拷貝 auth-service 模塊的 User、Role 和 UserRepository 三個類到本模塊。在 Service 層的 UserService 編寫壹個 插入用戶 的方法,代碼如下:
配置用於用戶密碼 加密 的工具類 BPwdEncoderUtil:
實現壹個 用戶註冊 的 API 接口 /user/register,代碼如下:
在 Service 層的 UserServiceDetail 中添加壹個 login() 方法,代碼如下:
AuthServiceClient 作為 Feign Client,通過向 auth-service 服務接口 /oauth/token 遠程調用獲取 JWT。在請求 /oauth/token 的 API 接口中,需要在 請求頭 傳入 Authorization 信息, 認證類型 ( grant_type )、用戶名 ( username ) 和 密碼 ( password ),代碼如下:
其中,AuthServiceHystrix 為 AuthServiceClient 的 熔斷器 ,代碼如下:
JWT 包含了 access_token、token_type 和 refresh_token 等信息,代碼如下:
UserLoginDTO 包含了壹個 User 和壹個 JWT 成員屬性,用於返回數據的實體:
登錄異常類 UserLoginException
全局異常處理 切面類 ExceptionHandle
在 Web 層的 UserController 類中新增壹個登錄的 API 接口 /user/login 如下:
依次啟動 eureka-service,auth-service 和 user-service 三個服務。
7. 使用Postman測試
因為沒有權限,訪問被拒絕。在數據庫手動添加 ROLE_ADMIN 權限,並與該用戶關聯。重新登錄並獲取 JWT,再次請求 /user/foo 接口。
在本案例中,用戶通過 登錄接口 來獲取 授權服務 加密後的 JWT。用戶成功獲取 JWT 後,在以後每次訪問 資源服務 的請求中,都需要攜帶上 JWT。 資源服務 通過 公鑰解密 JWT, 解密成功 後可以獲取 用戶信息 和 權限信息 ,從而判斷該 JWT 所對應的 用戶 是誰,具有什麽 權限 。
獲取壹次 Token,多次使用, 資源服務 不再每次訪問 授權服務 該 Token 所對應的 用戶信息 和用戶的 權限信息 。
壹旦 用戶信息 或者 權限信息 發生了改變,Token 中存儲的相關信息並 沒有改變 ,需要 重新登錄 獲取新的 Token。就算重新獲取了 Token,如果原來的 Token 沒有過期,仍然是可以使用的。壹種改進方式是在登錄成功後,將獲取的 Token 緩存 在 網關上 。如果用戶的 權限更改 ,將 網關 上緩存的 Token 刪除 。當請求經過 網關 ,判斷請求的 Token 在 緩存 中是否存在,如果緩存中不存在該 Token,則提示用戶 重新登錄 。