SpringBoot3 登录注册基础篇(三) - JJWT 0.12 核心 API 详解

SpringBoot3 登录注册基础篇(三) - JJWT 0.12 核心 API 详解

第一章. JWT 三段式结构

在开始编写代码之前,我们需要先理解:JWT 到底是什么?它是如何工作的?

1.1. 电影票的类比

假设你现在去电影院看电影。你在售票处买票后,工作人员会给你一张电影票。这张票上印着:

  • 电影名称(如《流浪地球 3》)
  • 场次时间(如 2025-12-27 20:00)
  • 座位号(如 5 排 8 座)
  • 一个防伪水印(防止别人伪造票)

当你进入影厅时,检票员只需要看一眼票上的防伪水印,就知道这张票是真的,不需要回到售票处再次确认。

JWT 的工作原理和这张电影票非常相似:

  • 电影票 = JWT Token
  • 票上的信息(电影名称、场次、座位)= JWT 的 Payload(载荷)
  • 防伪水印 = JWT 的 Signature(签名)
  • 检票员 = 业务服务(验证 Token)

1.2. JWT 的三段式结构

一个完整的 JWT Token 由三部分组成,用 . 分隔:

1
2
3
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3MzUyODAwMDB9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

这三部分分别是:

第一部分:Header(头部)

1
2
3
4
{
"alg": "RS256",
"typ": "JWT"
}
  • alg:签名算法(Algorithm),这里是 RS256
  • typ:类型(Type),固定为 JWT

这部分经过Base64Url 编码一种 URL 安全的 Base64 编码方式后,就是 Token 的第一段。

第二部分:Payload(载荷)

1
2
3
4
5
{
"userId": 1001,
"username": "admin",
"exp": 1735280000
}
  • userId:用户 ID(自定义字段)
  • username:用户名(自定义字段)
  • exp:过期时间(Expiration,标准字段)

这部分经过 Base64Url 编码后,就是 Token 的第二段。

第三部分:Signature(签名)

1
2
3
4
Signature = RSA_Sign(
Base64Url(Header) + "." + Base64Url(Payload),
私钥
)

签名的作用是 防止 Token 被篡改。如果有人修改了 Payload 中的 userId,签名验证就会失败。


第二章. JJWT 0.12 vs 旧版本

2.1. 为什么要强调版本

JJWT 0.12 引入了 类型安全 的设计理念,API 发生了重大变化。如果你直接照搬网上的旧教程,代码很可能会报错。

旧版本(0.9.x)的写法

1
2
3
4
5
6
7
8
// ❌ 旧版本写法(存在隐患)
Jwts.builder()
.

signWith(SignatureAlgorithm.HS256, rsaPrivateKey) // 运行时才会报错!
.

compact();

这种写法的致命伤在于:算法与密钥类型分离。你指定了对称加密算法 HS256,却传入了非对称加密的 RSA私钥
。编译器无法发现这个逻辑错误,只有在代码运行到这一行时才会抛出异常。

新版本(0.12.x)的写法

1
2
3
4
5
6
7
8
// ✅ 新版本写法(类型安全)
Jwts.builder()
.

signWith(rsaPrivateKey, Jwts.SIG.RS256) // 编译期检查
.

compact();

新版本强制要求你只传入密钥,算法由库自动推断或通过强类型常量指定。这样编译器就能在编译期发现错误。

2.2. 核心 API 对比

对比维度旧版本(0.9.x)新版本(0.12.x)
签名方式signWith(Algorithm, Key)signWith(Key, Algorithm)
类型安全❌ 运行时才报错✅ 编译期检查
解析器构建直接使用必须调用 build()
算法常量SignatureAlgorithm.RS256Jwts.SIG.RS256

第三章. 封装 JwtUtil 工具类

3.1. 配置类设计

首先,我们需要定义一个配置类来承载 JWT 的相关配置(如有效期、密钥路径等)。

📄 文件路径auth-core/src/main/java/com/example/auth/core/config/properties/JwtProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.auth.core.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {

/**
* 签发者标识 (Issuer)
*/
private String issuer = "auth-service";

/**
* Access Token 有效期(分钟)
*/
private Integer accessTokenExpireMinutes = 15;

/**
* 时钟偏差容忍度(秒)
*/
private Long clockSkewSeconds = 30L;

/**
* 公钥资源路径
*/
private String publicKeyResource = "certs/public_key.pem";

/**
* 私钥资源路径
*/
private String privateKeyResource = "certs/private_key.pem";
}

auth-web/src/main/resources/application.yml 中添加对应的配置:

1
2
3
4
5
6
jwt:
issuer: pro-auth-service
access-token-expire-minutes: 15
clock-skew-seconds: 30
public-key-resource: certs/public_key.pem
private-key-resource: certs/private_key.pem

3.2. JwtUtil 核心代码

📄 文件路径auth-core/src/main/java/com/example/auth/core/util/JwtUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package com.example.auth.core.util;

import cn.hutool.core.util.IdUtil;
import com.example.auth.common.util.crypto.RsaKeyManager;
import com.example.auth.core.config.properties.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
import java.util.Map;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {

private final JwtProperties jwtProperties;
private final RsaKeyManager rsaKeyManager;

// 缓存解析后的密钥对象,避免每次请求重复解析
private PrivateKey privateKey;
private PublicKey publicKey;


/**
* 初始化阶段:加载密钥
*/
@PostConstruct
public void init() throws Exception {
// 读取加载公钥和私钥内容
String privateKeyContent = rsaKeyManager.readResourceFile(jwtProperties.getPrivateKeyResource());
this.privateKey = rsaKeyManager.loadPrivateKey(privateKeyContent);
String publicKeyContent = rsaKeyManager.readResourceFile(jwtProperties.getPublicKeyResource());
this.publicKey = rsaKeyManager.loadPublicKey(publicKeyContent);
log.info("JWT 密钥加载完成。");
}

/**
* 创建 Token
* @param userId 用户 ID
* @param username 用户名
* @param extraClaims 额外的自定义声明
* @return 生成的 JWT 字符串
*/
public String createToken(Long userId, String username, Map<String, Object> extraClaims) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);

// 计算过期时间
Date expiration = new Date(nowMillis + jwtProperties.getAccessTokenExpireMinutes() * 60 * 1000L);
// 生成 JTI(用于后续的黑名单功能)
String jti = IdUtil.fastSimpleUUID();
JwtBuilder builder = Jwts.builder()
.header().type("JWT").and() // 设置头部
.issuer(jwtProperties.getIssuer()) // 签发者
.subject(userId.toString()) // 主题(用户 ID)
.issuedAt(now) // 签发时间
.expiration(expiration) // 过期时间
.id(jti) // JWT ID
.claim("username", username) // 自定义声明:用户名
.claim("userId", userId); // 自定义声明:用户 ID
// 添加额外的载荷
if (extraClaims != null && !extraClaims.isEmpty()) {
builder.claims().add(extraClaims);
}
// 使用 RSA 私钥 + RS256 算法签名
return builder.signWith(privateKey, Jwts.SIG.RS256).compact();
}

/**
* 解析并验证 Token
* @param token JWT 字符串
* @return 解析后的 Claims 对象
*/
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(publicKey)
.clockSkewSeconds(jwtProperties.getClockSkewSeconds())
.build()
.parseSignedClaims(token)
.getPayload();
}

/**
* 快速验证方法
* @param token JWT 字符串
* @return boolean 验证结果
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.debug("Token验证失败: {}", e.getMessage());
return false;
}
}

/**
* 从 Token 中提取用户 ID
* @param token JWT 字符串
* @return 用户 ID
*/
public Long getUserIdFromToken(String token) {
try {
Claims claims = parseToken(token);
return Long.parseLong(claims.getSubject());
} catch (Exception e) {
log.warn("从Token中提取用户ID失败: {}", e.getMessage());
return null;
}
}

/**
* 从 Token 中提取用户名
* @param token JWT 字符串
* @return 用户名
*/
public String getUsernameFromToken(String token) {
try {
Claims claims = parseToken(token);
return claims.get("username", String.class);
} catch (Exception e) {
log.warn("从Token中提取用户名失败: {}", e.getMessage());
return null;
}
}

/**
* 从 Token 中提取 JTI
* @param token JWT 字符串
* @return JTI
*/
public String getJtiFromToken(String token) {
try {
Claims claims = parseToken(token);
return claims.getId();
} catch (Exception e) {
log.warn("从Token中提取JTI失败: {}", e.getMessage());
return null;
}
}
}

3.3. 关键设计细节

时钟偏差(Clock Skew)的必要性

在代码中我们设置了 .clockSkewSeconds(30)。这是一个非常重要的生产环境细节。

在微服务架构中,认证服务和网关服务可能部署在不同的物理机上,时间可能存在微小的偏差。如果认证服务时间快了 1 秒,它签发的
Token 对于网关来说就是 “未来 1 秒才生效” 的,会导致 PrematureJwtException。设置容忍度可以完美解决这个问题。

JTI(JWT ID)的战略意义

我们在创建 Token 时使用 Hutool 的 IdUtil.fastSimpleUUID() 生成了 JTI 并赋值给 .id(jti)。这不仅仅是一个随机数,它是后续实现
Token 黑名单(Token Revocation)的基础。当用户注销时,我们只需将这个 JTI 放入 Redis,即可让未过期的 Token 提前失效。

@PostConstruct 的性能优化

我们在 init() 方法上使用了 @PostConstruct 注解。这意味着密钥的加载和解析只会在应用启动时执行一次,然后缓存在
privateKeypublicKey 字段中。

如果不这样做,每次生成或验证 Token 都需要重新读取文件、解析 PEM 格式,性能会非常差。


第四章. 本章小结

我们完成了 JWT 工具类的构建,掌握了 JJWT 0.12 的核心 API。

核心成果

步骤操作产出
1理解 JWT 三段式结构Header + Payload + Signature
2掌握 JJWT 0.12 Builder 模式类型安全的 API
3封装 JwtUtil 工具类提供生成、解析、验证功能

方法速查

类名方法名作用
JwtUtilcreateToken(userId, username, extraClaims)生成 Token
JwtUtilparseToken(token)解析 Token
JwtUtilvalidateToken(token)验证 Token
JwtUtilgetUserIdFromToken(token)提取用户 ID
JwtUtilgetUsernameFromToken(token)提取用户名
JwtUtilgetJtiFromToken(token)提取 JTI

JJWT 0.12 vs 旧版本

对比维度旧版本(0.9.x)新版本(0.12.x)
签名方式signWith(Algorithm, Key)signWith(Key, Algorithm)
类型安全❌ 运行时才报错✅ 编译期检查
解析器构建直接使用必须调用 build()