概述 JWT,Java Web Token,通過 JSON 形式作為 Web 應用中的令牌,用於在各方之間安全地將信息作為 JSON 對象傳輸,在數據傳輸過程中還可以完成數據加密、簽名等相關處理 JWT 的作用如下: 授權:一旦用戶登錄,每個後續請求將包括 JWT,從而允許用戶訪問該令牌允許的路由, ...
概述
JWT,Java Web Token,通過 JSON 形式作為 Web 應用中的令牌,用於在各方之間安全地將信息作為 JSON 對象傳輸,在數據傳輸過程中還可以完成數據加密、簽名等相關處理
JWT 的作用如下:
- 授權:一旦用戶登錄,每個後續請求將包括 JWT,從而允許用戶訪問該令牌允許的路由,服務和資源
- 信息交換:JSON Web Token 是在各方之間安全地傳輸信息的好方法,因為可以對 JWT 進行簽名,此外,由於簽名是使用標頭和有效負載計算的,因此還可以驗證內容是否篡改
傳統的 Session 認證
1. 認證方式
http 協議本身是一種無狀態協議,這就意味著如果用戶向我們的應用提供了用戶名和密碼進行認證,那麼下次請求時還需再作一次認證。因為根據 http 協議,我們並不知道是哪個用戶發出的請求,所以為了讓應用能識別是哪個用戶發出的請求,我們只能在服務存儲一份用戶登錄的信息,這份登錄信息會在響應傳遞給瀏覽器,告訴其保存為 cookie,以便下次請求時發送回來,這樣我們就能識別請求來自哪個用戶了
2. 缺點
- 每個用戶經過認證後,都要在服務端做一次記錄,以方便下次鑒別。通常而言,session 都是保存在記憶體中的,隨著認證用戶的增多,服務端的開銷也會明顯增大
- 用戶認證之後,服務端做認證記錄,如果認證的記錄被保存到記憶體中,就意味著下次用戶請求還得訪問該伺服器,才能拿到授權的資源,在分散式應用上,相應的限制了負載均衡器的能力,也就意味著限制了應用的擴展能力
- 基於 cookie 進行用戶識別,cookie 一旦被捕獲,用戶就會很容易受到跨站請求偽造的攻擊
- 在前後端分離時增加了部署的複雜性
JWT 認證
1. 認證方式
- 前端通過 Web 表單將自己的用戶名和密碼發送給後端的介面,這一過程一般是 http post 請求,建議的方式是通過 SSL 加密的傳輸(https),從而避免敏感信息被嗅探
- 後端核對用戶名和密碼成功後,將用戶的 id 等其他信息作為 JWT Payload(負載),將其與頭部分別進行 Base64 編碼,拼接後簽名,形成一個 JWT Token
- 後端將 JWT 字元串作為登錄成功的返回結果返回,前端可以將返回的結果保存在本地緩存上,退出登錄時前端刪除保存的 JWT 即可
- 前端在每次請求後端帶回 JWT
- 後端檢查是否存在,如驗證 JWT 的有效性,檢查簽名是否正確,檢查 Token 是否過期,檢查 Token 接收方是否是自己
- 驗證通過後,後端使用 JWT 中包含的用戶信息進行其他邏輯操作,返回相應結果
2. 優點
- 簡潔,可以通過 URL、POST 參數或者在 HTTP Header 發送,因為數據量小,傳輸速度快
- 自包含,負載中包含了所有用戶需要的信息,避免多次查詢資料庫
- Token 以加密形式保存在客戶端,原則上任何 web 形式都支持
- 不需要在服務端保存會話信息,特別適用於分散式微服務
JWT 結構
通常 JWT 如下所示:xxxxx.yyyyy.zzzzz
-
標頭(Header):標頭一般由兩部分組成:令牌的類型和所使用的簽名演算法,標頭使用 Base64 編碼
{ "alg":"HS256", "typ":"JWT" }
-
有效負載(payload):令牌的第二部分是有效負載,其中包含聲明。聲明是有關實體(通常是用戶)和其他數據的聲明。同樣使用 Base64 編碼。不建議放入用戶的敏感信息
{ "sub":"12345678", "name":"john", "admin":true, ... }
-
簽名(Signature):Signature 需要使用編碼後的 Header 和 Payload 以及我們提供的一個密鑰,然後使用 Header 中指定簽名演算法,進行簽名。簽名的作用是保證 JWT 沒有被篡改過,如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
客戶端收到服務端發送的 token 後,再次請求服務端需要帶上 token,此時 token 包含三部分:經過 Base64 編碼的 Header、經過 Base64 編碼的 Payload 和加密後的簽名,服務端用自己保存的 secret 與客戶端發送的 Header、Payload 運算,如果結果和客戶端帶回來的簽名不一致,則驗證失敗
JWT 使用
引入依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
生成 token
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 20);
Map<String, Object> map = new HashMap<>();
String token = JWT.create().withHeader(map) // header
.withClaim("userId", 21) // payload
.withClaim("username", "yeeq")
.withExpiresAt(instance.getTime()) // 指定令牌的過期時間
.sign(Algorithm.HMAC256("FAWF2#!F@")); // 簽名,並指定密鑰
根據令牌和簽名解析數據
// 創建驗證對
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("FAWF2#!F@")).build();
// 解碼後的信息
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDAyNTQ4NzIsInVzZXJJZCI6MjEsInVzZXJuYW1lIjoieWVlcSJ9.jo_6gKThSXUcEfH1e9bu7at9lm2zmdupwiYvMUWopls");
System.out.println(verify.getClaim("userId").asInt());
System.out.println(verify.getClaim("username").asString());
常見異常信息:
SignatureVerificationException
:簽名不一致異常TokenExpiredException
:令牌過期異常AlgorithmMismatchException
:演算法不匹配異常InvalidClaimException
:失效的 payload 異常
JWT 封裝工具類
一般結合攔截器或者網關完成認證
public class JWTUtils {
private static final String SIGN = "FAWF2#!F@";
/**
* 生成 token
* @param map payload 的信息
* @return token
*/
public static String getToken(Map<String, String> map) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 7);
JWTCreator.Builder builder = JWT.create();
// 創建 payload
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
String token = builder.withExpiresAt(instance.getTime()) // 指定令牌的過期時間
.sign(Algorithm.HMAC256(SIGN)); // 簽名,並指定密鑰
return token;
}
/**
* 驗證 token 的合法性
* @param token token
*/
public static void verifyJWT(String token) {
JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
/**
* 獲取 token 的信息
* @return token 的信息
*/
public static DecodedJWT getTokenInfo(String token) {
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
}