聲明:這隻是一個入門級的JWT示例教學,歡迎各位朋友踴躍發言,一起探讨進步。
聲明:本文不介紹JWT相關概念,也不比較JWT的各個類庫,對相關概念、JWT各個類庫感興趣的朋友可
自行查閱相關資料;本文直接示範示例使用入門級JWT。
提示:本人較懶,不想什麼基本的東西都自己寫,是以這裡使用了JWT衆多類庫中的nimbus-jose-jwt類庫。
本文中涉及到的JwtUtil工具類,其實是本人對nimbus-jose-jwt提供的基本功能的一個簡單封裝。
軟硬體環境說明:Windows10、IntelliJ IDEA、SpringBoot 2.1.6.RELEASE。
準備工作:在pom.xml中映入依賴

本人為了及快速開發、為了示例,還引入了其他的一些不是關鍵的依賴檔案。給出完整pom.xml文字版:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.szlzcl</groupId>
<artifactId>jwt-token</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwt-token</name>
<description>測試使用json web token</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>7.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
JWT簡單使用示例:
先給出本人的示例項目結構:
各個檔案的具體内容:
- JwtUtil:
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import java.text.ParseException;
/**
* 對nimbus-jose-jwt類庫提供的基本功能 再進行工具類封裝
*
* @author JustryDeng
* @date 2019/7/21 13:46
*/
@SuppressWarnings("unused")
public class JwtUtil {
/**
* 生成token -- 采用【對稱加密】算法HS256驗證簽名
*
* 注:此方法生成的jwt的Header部分為預設的
* {
* "alg": "HS256",
* "typ": "JWT"
* }
*
* @param payloadJsonString
* 有效負載JSON字元串
*
* @param secret
* 對稱加密/解密密鑰
* 注意:secret.getBytes().length 必須 >=32
*
* @return token
* @throws JOSEException
* 以下情況會抛出此異常:
* 1、密鑰長度 secret.getBytes().length < 32時,會抛出此異常
* 2、JWS已簽名
* 3、JWS無法使用指定的簽名器
* @date 2019/7/21 13:54
*/
public static String generateToken(String payloadJsonString, String secret)
throws JOSEException {
// 建立Header(設定以JWSAlgorithm.HS256算法進行簽名認證)
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
// 建立Payload
Payload payload = new Payload(payloadJsonString);
/// Signature相關
// 根據Header以及Payload建立JSON Web Signature (JWS)對象
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 使用給的對稱加密密鑰,建立一個加密器
JWSSigner jwsSigner = new MACSigner(secret);
// 将該簽名器 與 JSON Web Signature (JWS)對象進行關聯
// 即: 指定JWS使用該簽名器進行簽名
jwsObject.sign(jwsSigner);
// 使用JWS生成JWT(即:使用JWS生成token)
return jwsObject.serialize();
}
/**
* 校驗token是否被篡改,并傳回有效負載JSON字元串 -- 采用【對稱加密】算法HS256驗證簽名
*
* @param secret
* 對稱加密/解密密鑰
* 注意:secret.getBytes().length 必須 >=32
*
* @return 有效負載JSON字元串
* @throws JOSEException,ParseException,JwtSignatureVerifyException 異常資訊
* @date 2019/7/21 14:08
*/
public static String verifySignature(String token, String secret)
throws JOSEException, ParseException, JwtSignatureVerifyException {
// 解析token,将token轉換為JWSObject對象
JWSObject jwsObject = JWSObject.parse(token);
// 建立一個JSON Web Signature (JWS) verifier.用于校驗簽名(即:校驗token是否被篡改)
JWSVerifier jwsVerifier = new MACVerifier(secret);
// 如果校驗到token被篡改(即:簽名認證失敗),那麼抛出異常
if(!jwsObject.verify(jwsVerifier)) {
throw new JwtSignatureVerifyException("Signature verification result is fail!");
}
// 擷取有效負載
Payload payload = jwsObject.getPayload();
// 傳回 有效負載JSON字元串
return payload.toString();
}
/**
* 生成token -- 采用【非對稱加密】算法RS256驗證簽名
*
* @param payloadJsonString
* 有效負載JSON字元串
*
* @param rsaKey
* 非對稱加密密鑰對
* 提示:RSAKey示例,可以這麼獲得
* RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(1024 * 3);
* RSAKey rsaKey = rsaKeyGenerator.generate();
*
* @return token
* @throws JOSEException 異常資訊
* @date 2019/7/21 13:54
*/
public static String generateTokenByAsymmetric(String payloadJsonString, RSAKey rsaKey)
throws JOSEException {
// Header
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(rsaKey.getKeyID())
.build();
// Payload
Payload payload= new Payload(payloadJsonString);
/// Signature相關
// 根據Header以及Payload建立JSON Web Signature (JWS)對象
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 使用給的對稱加密密鑰,建立一個加密器
JWSSigner signer = new RSASSASigner(rsaKey);
// 将該簽名器 與 JSON Web Signature (JWS)對象進行關聯
// 即: 指定JWS使用該簽名器進行簽名
jwsObject.sign(signer);
// 使用JWS生成JWT(即:使用JWS生成token)
return jwsObject.serialize();
}
/**
* 校驗token是否被篡改,并傳回有效負載JSON字元串 -- 采用【非對稱加密】算法RS256驗證簽名
*
* @param rsaKey
* 非對稱加密密鑰對
*
* @return 有效負載JSON字元串
* @throws JOSEException,ParseException,JwtSignatureVerifyException 異常資訊
* @date 2019/7/21 14:08
*/
public static String verifySignatureByAsymmetric(String token, RSAKey rsaKey)
throws JOSEException, ParseException, JwtSignatureVerifyException {
// 根據token獲得JSON Web Signature (JWS)對象
JWSObject jwsObject = JWSObject.parse(token);
// 擷取到公鑰
RSAKey publicRsaKey = rsaKey.toPublicJWK();
// 根據公鑰 擷取 Signature驗證器
JWSVerifier jwsVerifier = new RSASSAVerifier(publicRsaKey);
// 如果校驗到token被篡改(即:簽名認證失敗),那麼抛出異常
if(!jwsObject.verify(jwsVerifier)) {
throw new JwtSignatureVerifyException("Signature verification result is fail!");
}
// 擷取有效負載
Payload payload = jwsObject.getPayload();
// 傳回 有效負載JSON字元串
return payload.toString();
}
// /**
// * main方法測試
// *
// * --------------------------------------下面的為測試代碼--------------------------------------
// *
// */
// public static void main(String[] args)
// throws JOSEException, ParseException, JwtSignatureVerifyException {
// // 測試對稱加密算法 驗證簽名的JWT
// testSymmetric();
// // 測試非對稱加密算法 驗證簽名的JWT
// testAsymmetric();
// }
//
// /**
// * 測試對稱加密算法的token生成與 簽名檢驗
// */
// private static void testSymmetric()
// throws JOSEException, ParseException, JwtSignatureVerifyException {
// String secret = "adsgfiaughofashdofhjasodhfoasdafisd";
// String token = JwtUtil.generateToken("{\"name\":\"張三\"}", secret);
// System.out.println(token);
// String payloadJsonString = JwtUtil.verifySignature(token, secret);
// System.out.println(payloadJsonString);
// }
//
// /**
// * 測試對稱加密算法的token生成與 簽名檢驗
// */
// private static void testAsymmetric()
// throws JOSEException, ParseException, JwtSignatureVerifyException {
// RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(1024 * 3);
// RSAKey rsaKey = rsaKeyGenerator.generate();
// String token =
// JwtUtil.generateTokenByAsymmetric("{\"name\":\"張三\"}", rsaKey);
// System.out.println(token);
// String payloadJsonString = JwtUtil.verifySignatureByAsymmetric(token, rsaKey);
// System.out.println(payloadJsonString);
// }
}
- JwtSignatureVerifyException:
/**
* 簽名校驗失敗異常
* 即:token被篡改異常
*
* @author JustryDeng
* @date 2019/7/21 14:23
*/
@SuppressWarnings("unused")
public class JwtSignatureVerifyException extends Exception {
private static final long serialVersionUID = -861994790728930634L;
/**
* Creates a new JwtSignatureVerifyException with the specified message.
*
* @param message The exception message.
*/
public JwtSignatureVerifyException(String message) {
super(message);
}
/**
* Creates a new JwtSignatureVerifyException with the specified message and cause.
*
* @param message The exception message.
* @param cause The exception cause.
*/
public JwtSignatureVerifyException(String message, Throwable cause) {
super(message, cause);
}
}
- PayloadDTO:
import lombok.*;
import java.io.Serializable;
/**
* JWT中間部分 有效負載 資料模型
*
* @author JustryDeng
* @date 2019/7/21 15:31
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PayloadDTO implements Serializable {
private static final long serialVersionUID = 5988619597830511341L;
/**
* ------------------------------------這部分為 注冊聲明------------------------------------
* 這個jwt的身份id
*/
private String jti;
/** 簽發人 */
private String iss;
/** 過期時長(機關ms) */
private Long exp;
/** 主題 */
private String sub;
/** 閱聽人 */
private String aud;
/** 生效時間(1970年1月1日到現在的偏移量) */
private Long nbf;
/** 簽發時間(1970年1月1日到現在的偏移量) */
private Long iat;
/**
* ------------------------------------這部分為 公開聲明------------------------------------
* 姓名
*/
private String name;
/**
* 性别
*/
private String gender;
/**
* 出生日期
*/
private String birthday;
/**
* ------------------------------------這部分為 私有聲明------------------------------------
* 是否是管理者
*/
private Boolean isAdmin;
}
- application.properties:
# 驗證簽名的(對稱加密)算法的密鑰
my.jwt.signature.algorithm.secret=abcdefghijklmnopqrstuvwxyz123456789
# 機關ms
my.jwt.expiration.time=60000
- DemoController:
import com.alibaba.fastjson.JSON;
import com.nimbusds.jose.JOSEException;
import com.szlzcl.jwttoken.model.PayloadDTO;
import com.szlzcl.jwttoken.util.JwtSignatureVerifyException;
import com.szlzcl.jwttoken.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.text.ParseException;
/**
* controller
*
* @author JustryDeng
* @date 2019/7/21 11:03
*/
@Slf4j
@RestController
public class DemoController {
@Value("${my.jwt.signature.algorithm.secret}")
private String signatureAlgorithmSecret;
@Value("${my.jwt.expiration.time}")
private Long tokenExpirationTime;
/**
* 使用者登入, 并傳回token資訊
*
* 注:真正使用時,往往除了token,還會傳回一些其他的相關資訊。
* 注:真正使用時,token往往放在響應頭裡面進行傳回。
* 注:真正使用時,使用者登入不應用get方法,而是用post方法。
*
* @param name 使用者名
* @param password mima
*
* @return 傳回jwt
* @date 2019/7/21 11:05
*/
@GetMapping("/login")
@SuppressWarnings("all")
public String login(@RequestParam("name") String name,
@RequestParam("password") String password){
// 模拟 使用者名密碼 均正确
if("鄧沙利文".equals(name) && "123xyz".equals(password)) {
/*
* 模拟獲得有效負載資訊
* 注:根據自己業務的不同,往往這一步會有非常大的不同
*/
PayloadDTO payloadDTO = getPayload(name);
// 生成token
String payloadJsonString = JSON.toJSONString(payloadDTO);
String token;
try {
token = JwtUtil.generateToken(payloadJsonString, signatureAlgorithmSecret);
} catch (JOSEException e) {
log.error("生成token失敗!", e);
return "生成token失敗";
}
return token;
}
return "使用者名密碼有誤!";
}
/**
* 驗證請求中的token是否被篡改、是否過期(是否有效)。
* 如果有效那麼傳回響應的業務資訊。
*
* 注:真正使用時,token的驗證往往在Filter或AOP中進行。
* 注:真正使用時,一般将token放在請求頭中,以Authorization作為key,以Bearer <token>作為值。
*
* @param token json web token(JWT)
*
* @return 傳回相應資訊
* @date 2019/7/21 11:05
*/
@GetMapping("/test")
public String logis(@RequestHeader("Authorization") String token){
String payloadJsonString;
// 驗證token是否被篡改
try {
payloadJsonString = JwtUtil.verifySignature(token, signatureAlgorithmSecret);
} catch (JwtSignatureVerifyException e) {
log.error("token簽名驗證失敗, token已經被篡改!", e);
return "token簽名驗證失敗, token已經被篡改!";
} catch (JOSEException | ParseException e) {
log.error("驗證token簽名時,系統異常!", e);
return "驗證token簽名時,系統異常!";
}
/// 驗證 有效負載中的其他資訊 (如:過期時間、權限資訊 等等)
PayloadDTO payloadDTO =JSON.parseObject(payloadJsonString, PayloadDTO.class);
log.info("token中存放的使用者資訊是 -> {}", payloadDTO);
// token生效時間
long nbf = payloadDTO.getNbf();
log.info("token的生效時間是 -> {}", nbf);
// token有效時長
long exp = payloadDTO.getExp();
log.info("token的生效時長是 -> {}", exp);
// 目前時間
long nowTime = System.currentTimeMillis();
log.info("目前時間是 -> {}", nowTime);
boolean isAuthorized = (nowTime - nbf) >= 0 && exp >= (nowTime - nbf);
log.info("token -> 【{}】是否有效? {}", token, isAuthorized);
if (isAuthorized) {
return "token認證通過!";
}
return "token已過期,請重新登入";
}
/**
* 模拟生成 有效負載資訊
*
* 注:根據自己業務情況的不同,可能會往有效負載中放入不同的資訊;
* 此步驟的邏輯也可能會非常複雜。
*
* @date 2019/7/21 16:00
*/
private PayloadDTO getPayload(String name) {
return PayloadDTO.builder()
// 放置過期時長
.exp(tokenExpirationTime)
// 放置生效時間
.nbf(System.currentTimeMillis())
// 放置使用者資訊
.name(name)
.birthday("1994-02-05")
.isAdmin(true)
.build();
}
}
測試一下:
第一步:啟動項目,并通路localhost:8080/login?name=鄧沙利文&password=123xyz:
第二步:以Authorization為key,以将第一步拿到的token為value,放入請求Header裡面,并通路localhost:8080/test:
此時,我們不妨觀察一下IDEA的控制台輸出:
token中存放的使用者資訊是 -> PayloadDTO(jti=null, iss=null, exp=60000, sub=null, aud=null, nbf=1563700723248, iat=null, name=鄧沙利文, gender=null, birthday=1994-02-05, isAdmin=true)
token的生效時間是 -> 1563700723248
token的生效時長是 -> 60000
目前時間是 -> 1563700766988
token -> 【eyJhbGciOiJIUzI1NiJ9.eyJiaXJ0aGRheSI6IjE5OTQtMDItMDUiLCJleHAiOjYwMDAwLCJpc0FkbWluIjp0cnVlLCJuYW1lIjoi6YKT5rKZ5Yip5paHIiwibmJmIjoxNTYzNzAwNzIzMjQ4fQ.0nCrMAC8QO6r-dVnvQkzsSDlIWu3dxNwxOLnVs74saU】是否有效? true
第三步:等一分鐘,讓時間超過我們設定的有效時長(本人設定的是60000毫秒),再按照第二步的方式進行通路:
此時,我們不妨再觀察一下IDEA的控制台輸出:
token中存放的使用者資訊是 -> PayloadDTO(jti=null, iss=null, exp=60000, sub=null, aud=null, nbf=1563700723248, iat=null, name=鄧沙利文, gender=null, birthday=1994-02-05, isAdmin=true)
token的生效時間是 -> 1563700723248
token的生效時長是 -> 60000
目前時間是 -> 1563701012872
token -> 【eyJhbGciOiJIUzI1NiJ9.eyJiaXJ0aGRheSI6IjE5OTQtMDItMDUiLCJleHAiOjYwMDAwLCJpc0FkbWluIjp0cnVlLCJuYW1lIjoi6YKT5rKZ5Yip5paHIiwibmJmIjoxNTYzNzAwNzIzMjQ4fQ.0nCrMAC8QO6r-dVnvQkzsSDlIWu3dxNwxOLnVs74saU】是否有效? false
由此可見,入門級JWT使用并示例成功!
^_^ 如有不當之處,歡迎指正
^_^ 參考連結
http://andaily.com/blog/?p=956
https://www.jianshu.com/p/75208a68c3b9
https://www.sohu.com/a/250972011_575744
^_^ 測試代碼托管連結
https://github.com/JustryDeng...er/Abc_JwtToken_Demo