前言
說說JWT,先說下網際網路服務常見的兩種使用者認證方式:
session認證與Token認證
session認證
傳統的Session認證的大體流程可以表示為使用者提供使用者名稱和密碼登入後由伺服器儲存一份使用者登入資訊並傳遞給瀏覽器儲存為Cookie,並在下次請求中根據Cookie來識別使用者,但這種方式缺陷明顯:
- Session都是儲存在記憶體中,隨著認證使用者的增多,服務端的開銷明顯增大
- 儲存在記憶體中的Session限制了分散式的應用
- Cookie容易被截獲偽造
Token認證
Token 泛指身份驗證時使用的令牌,Token鑑權機制從某些角度而言與Cookie是一個作用,其目的是讓後臺知道請求是來自於受信的客戶端,其通過實現了某種演算法加密的Token字串來完成鑑權工作,其優點在於:
- 伺服器不需要儲存 Session 資料(無狀態),容易實現擴充套件
- 有效避免Cookie被截獲引發的CSRF攻擊
- 可以儲存一些業務邏輯所必要的非敏感資訊
- 便於傳輸,其構成非常簡單,位元組佔用小
JWT簡介
JWT定義
- JWT全稱為Json web token,也就是 Json 格式的 web token,可以這麼理解:
Token // 個人證件
JWT // 個人身份證
JWT資料結構
- JWT由三段字串組成,中間用.分隔,如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInNjb3BlIjo4LCJleHAiOjE3MTU3NDAyMjIsImlhdCI6MTYyOTM0MDIyMn0.wuRsF5wvLHbDF_21Pocas8SeXQ315rgBl6wm1LRL2bQ
- JWT 的三個部分依次如下:
Header(頭部)// Header 部分是一個 JSON 物件,描述 JWT 的後設資料,通常是下面的樣子。
Payload(負載)// Payload 部分是一個 JSON 物件,用來存放實際需要傳遞的資料
Signature(簽名)// Signature 部分是對前兩部分的簽名,防止資料篡改
- 第一段字串Header:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
將其 Base64 解碼後得到:
{
"typ": "JWT", // TOKEN TYPE ,token型別
"alg": "HS256" //ALGORITHM,演算法 雜湊256
}
- 第二段字串Payload:
eyJ1aWQiOjEsInNjb3BlIjo4LCJleHAiOjE3MTU3NDAyMjIsImlhdCI6MTYyOTM0MDIyMn0
PAYLOAD是資料載體,可以有自定義資料
{
"uid": "1234567890" // 自定義資料
}
- 第三段字串Signature:
wuRsF5wvLHbDF_21Pocas8SeXQ315rgBl6wm1LRL2bQ
Signature 部分是對前兩部分的簽名,防止資料篡改。
JWT的類庫
- Java 中的 JWT 有很多類庫,關於其優缺點可以在官網檢視:https://jwt.io/,這裡我們介紹Auth0的JWT的整合使用方式
Auth0 實現的 com.auth0 / java-jwt / 3.3.0
Brian Campbell 實現的 org.bitbucket.b_c / jose4j / 0.6.3
connect2id 實現的 com.nimbusds / nimbus-jose-jwt / 5.7
Les Hazlewood 實現的 io.jsonwebtoken / jjwt / 0.9.0
FusionAuth 實現的 io.fusionauth / fusionauth-jwt / 3.1.0
Vert.x 實現的 io.vertx / vertx-auth-jwt / 3.5.1
具體實現
JWT配置
- pom.xml
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
- application.yml
coisini:
security:
jwt-key: coisini
# 過期時間
token-expired-in: 86400000
JWT工具類
- JwtUtil.java
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* @Description JWT工具類
* @author coisini
* @date Aug 18, 2021
* @Version 1.0
*/
@Component
public class JwtUtil {
/**
* key
*/
private static String jwtKey;
/**
* 過期時間
*/
private static Integer expiredTimeIn;
/**
* JWT KEY
* @param jwtKey
*/
@Value("${coisini.security.jwt-key}")
public void setJwtKey(String jwtKey) {
JwtUtil.jwtKey = jwtKey;
}
/**
* 過期時間
* @param expiredTimeIn
*/
@Value("${coisini.security.token-expired-in}")
public void setExpiredTimeIn(Integer expiredTimeIn) {
JwtUtil.expiredTimeIn = expiredTimeIn;
}
/**
* 生成令牌
* @param uid 使用者id
* @return
*/
public static String makeToken(Long uid) {
return JwtUtil.getToken(uid);
}
/**
* 獲取令牌
* @param uid 使用者id
* @param scope 許可權分級數字
* @return
*/
private static String getToken(Long uid) {
// 指定演算法
Algorithm algorithm = Algorithm.HMAC256(JwtUtil.jwtKey);
Map<String, Date> dateMap = JwtUtil.calculateExpiredIssues();
/**
* withClaim() 寫入自定義資料
* withExpiresAt() 設定過期時間
* withIssuedAt() 設定當前時間
* sign() 簽名演算法
*/
return JWT.create()
.withClaim("uid", uid)
.withExpiresAt(dateMap.get("expiredTime"))
.withIssuedAt(dateMap.get("now"))
.sign(algorithm);
}
/**
* 獲取自定義資料
* @param token
* @return
*/
public static Optional<Map<String, Claim>> getClaims(String token) {
DecodedJWT decodedJWT;
// 指定演算法
Algorithm algorithm = Algorithm.HMAC256(JwtUtil.jwtKey);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
try {
decodedJWT = jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
return Optional.empty();
}
return Optional.of(decodedJWT.getClaims());
}
/**
* 驗證Token
* @param token
* @return
*/
public static boolean verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(JwtUtil.jwtKey);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
return false;
}
return true;
}
/**
* 計算過期時間
* @return
*/
private static Map<String, Date> calculateExpiredIssues() {
Map<String, Date> map = new HashMap<>();
Calendar calendar = Calendar.getInstance();
Date now = calendar.getTime();
calendar.add(Calendar.SECOND, JwtUtil.expiredTimeIn);
// 當前時間
map.put("now", now);
// 過期時間
map.put("expiredTime", calendar.getTime());
return map;
}
}
測試介面
- JwtController.java
@RestController
@RequestMapping("/jwt")
public class JwtController {
/**
* 獲取Token
* @param id
* @return
*/
@GetMapping(value = "/get")
public String getToken(@RequestParam Long id) {
return JwtUtil.makeToken(id);
}
/**
* 驗證Token
* @param token
* @return
*/
@PostMapping("/verify")
public Map<String, Boolean> verify(@RequestParam String token) {
Map<String, Boolean> map = new HashMap<>();
Boolean valid = JwtUtil.verifyToken(token);
map.put("is_valid", valid);
return map;
}
}
- 測試結果
- JWT生成的Token應該放在請求頭內來傳輸,後端統一攔截驗證,這裡留在下篇文章吧。。。