Java JJWT 完全指南:从入门到生产级实践
前言
在分布式系统、微服务和前后端分离架构大行其道的今天,用户身份认证是系统安全的第一道防线。传统的 Session 认证在分布式环境中面临共享存储的难题,而 JWT(JSON Web Token)凭借其无状态、自包含、跨域友好的特性,成为了当前最主流的身份认证方案之一。
在 Java 生态中,JJWT(Java JWT)是最流行、最成熟的 JWT 实现库之一。本文将带你从零开始,全面深入地掌握 JJWT 的方方面面。
一、JWT 基础介绍
1.1 什么是 JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以 JSON 对象的形式安全地传输信息。它通常用于用户身份认证、信息交换、单点登录(SSO)和 API 接口鉴权等场景。
1.2 JWT 的核心优势
- 无状态(Stateless) :服务端无需存储会话信息,每个请求都携带完整的认证信息
- 自包含(Self-contained) :所有用户信息都包含在令牌中,无需额外查询数据库
- 跨域支持:天然支持前后端分离、微服务架构
- 可扩展性好:支持自定义声明(Claims)
1.3 JWT 的结构
一个 JWT 令牌由三部分组成,用 . 分隔:xxxxx.yyyyy.zzzzz
Header(头部) :包含令牌类型和签名算法
json
{
"alg": "HS256",
"typ": "JWT"
}
Payload(负载) :包含声明(Claims),即关于用户和附加信息的 JSON 数据。声明分为三类:
- Registered Claims :预定义标准字段,如
exp(过期时间)、iss(签发人)、sub(主题)、aud(受众) - Public Claims:公共字段,建议使用命名空间
- Private Claims:自定义私有字段
⚠️ 重要:不要在 Payload 中存放敏感信息(如密码),因为 Payload 只是 Base64 编码,不是加密,任何人都可以解码查看。
Signature(签名) :用于验证令牌完整性和防止篡改。生成方式为:
scss
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)
二、JJWT 是什么
2.1 JJWT 的定义
JJWT(Java JWT)是基于 JWT、JWS、JWE、JWK 和 JWA RFC 规范的 Java 实现。它的目标是成为 JVM 和 Android 平台上最容易使用和理解的 JWT 创建与验证库。
JJWT 采用流畅的 Builder 风格 API,隐藏了大部分复杂性,让开发者能够以直观的方式操作 JWT。它完全开源,基于 Apache License 2.0。
2.2 JWT vs JJWT 的区别
很多人容易混淆这两个概念:
- JWT 是一个标准(RFC 7519),定义了令牌的格式和规范
- JJWT 是 JWT 标准的Java 实现库
简单说:JWT 是"规范",JJWT 是"实现"。就像 JDBC 是规范,MySQL Driver 是实现一样。
三、JJWT 用法详解
3.1 依赖配置
JJWT 从 0.12.x 版本开始采用了模块化设计,将功能拆分为三个核心模块:
| 模块 | 职责 | 作用域 |
|---|---|---|
jjwt-api |
接口契约层,定义所有方法签名 | compile |
jjwt-impl |
核心实现层,负责签名、验证等 | runtime |
jjwt-jackson/jjwt-gson |
JSON 序列化/反序列化插件 | runtime |
Maven 配置:
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
Gradle 配置:
gradle
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
⚠️ 常见错误 :只引入
jjwt-api而不引入实现模块,会导致运行时NoClassDefFoundError。
3.2 生成 Token(0.12.x 版本)
0.12.x 版本强制使用 java.security.Key 对象,不再支持字符串密钥。
java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
public class JwtUtil {
// 从配置中获取密钥字符串(至少32字节)
private static final String SECRET_STRING = "your-256-bit-secret-key-here-at-least-32-characters";
private static final SecretKey SECRET_KEY =
Keys.hmacShaKeyFor(SECRET_STRING.getBytes(StandardCharsets.UTF_8));
public static String generateToken(String userId, String username) {
return Jwts.builder()
.subject(userId) // 设置用户标识
.claim("username", username) // 自定义声明
.issuedAt(new Date()) // 签发时间
.expiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时后过期
.signWith(SECRET_KEY) // 使用 SecretKey 签名
.compact(); // 生成最终 token
}
}
3.3 解析和验证 Token
java
public class JwtUtil {
public static Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(SECRET_KEY) // 0.12.x 使用 verifyWith
.build() // 构建 parser
.parseSignedClaims(token) // 解析签名 claims
.getPayload(); // 获取 payload
}
public static boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (ExpiredJwtException e) {
// Token 已过期
return false;
} catch (SignatureException e) {
// 签名不匹配
return false;
} catch (MalformedJwtException e) {
// Token 格式错误
return false;
} catch (UnsupportedJwtException e) {
// 不支持的 JWT
return false;
} catch (IllegalArgumentException e) {
// 参数非法
return false;
}
}
}
3.4 使用非对称密钥(RS256)
java
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
public class JwtRsaUtil {
private static final KeyPair KEY_PAIR;
static {
try {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
KEY_PAIR = gen.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 使用私钥签名
public static String generateToken(String subject) {
return Jwts.builder()
.subject(subject)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(KEY_PAIR.getPrivate()) // 私钥签名
.compact();
}
// 使用公钥验证
public static Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(KEY_PAIR.getPublic()) // 公钥验证
.build()
.parseSignedClaims(token)
.getPayload();
}
}
四、完整案例:Spring Boot 3.x + JJWT 实现登录认证
4.1 项目结构
bash
src/main/java/com/example/jwt/
├── config/
│ └── JwtConfig.java # JWT 配置类
├── filter/
│ └── JwtAuthenticationFilter.java # JWT 认证过滤器
├── interceptor/
│ └── JwtInterceptor.java # JWT 拦截器
├── util/
│ └── JwtUtil.java # JWT 工具类
├── controller/
│ └── AuthController.java # 认证控制器
└── model/
└── UserInfo.java # 用户信息实体
4.2 配置文件(application.yml)
yaml
jwt:
secret: "your-256-bit-secret-key-here-at-least-32-characters"
expiration: 3600000 # 1小时(毫秒)
refresh-expiration: 604800000 # 7天
4.3 JWT 工具类(完整版)
java
package com.example.jwt.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretString;
@Value("${jwt.expiration}")
private Long expiration;
private SecretKey getSigningKey() {
byte[] keyBytes = secretString.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成 Token
public String generateToken(String userId, String username, Map<String, Object> extraClaims) {
return Jwts.builder()
.subject(userId)
.claim("username", username)
.claims(extraClaims)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
// 从 Token 提取所有 claims
public Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// 提取特定 claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String extractUserId(String token) {
return extractClaim(token, Claims::getSubject);
}
public String extractUsername(String token) {
return extractClaim(token, claims -> claims.get("username", String.class));
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 验证 Token
public Boolean validateToken(String token) {
try {
extractAllClaims(token);
return true;
} catch (ExpiredJwtException e) {
// Token 已过期
return false;
} catch (SignatureException | io.jsonwebtoken.security.SignatureException e) {
// 签名无效
return false;
} catch (MalformedJwtException e) {
// Token 格式错误
return false;
} catch (UnsupportedJwtException e) {
// 不支持的 JWT
return false;
} catch (IllegalArgumentException e) {
// 参数非法
return false;
}
}
public Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
4.4 认证过滤器
java
package com.example.jwt.filter;
import com.example.jwt.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (jwtUtil.validateToken(token)) {
String userId = jwtUtil.extractUserId(token);
request.setAttribute("userId", userId);
request.setAttribute("username", jwtUtil.extractUsername(token));
}
}
filterChain.doFilter(request, response);
}
}
4.5 登录控制器
java
package com.example.jwt.controller;
import com.example.jwt.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public Map<String, String> login(@RequestParam String username,
@RequestParam String password) {
// 实际项目中需要验证用户名密码
// 这里简化处理,假设登录成功
Map<String, Object> claims = new HashMap<>();
claims.put("role", "USER");
claims.put("loginTime", System.currentTimeMillis());
String token = jwtUtil.generateToken("123456", username, claims);
Map<String, String> response = new HashMap<>();
response.put("token", token);
response.put("tokenType", "Bearer");
return response;
}
}
五、JJWT 原理深度解析
5.1 签名与验证原理
JWT 的安全性核心在于签名(Signature) 。
签名生成过程:
- 将 Header 和 Payload 分别进行 Base64Url 编码
- 将编码后的两部分用
.连接 - 使用 Header 中指定的算法和密钥对连接后的字符串进行签名
- 将签名结果进行 Base64Url 编码,作为第三部分
验证过程:
- 从 Token 中提取 Header 和 Payload 的 Base64Url 编码部分
- 使用相同的算法和密钥重新计算签名
- 将计算结果与 Token 中的 Signature 部分比较
- 如果一致,说明 Token 未被篡改
5.2 Base64Url 编码
JWT 使用 Base64Url 编码而非标准 Base64。区别在于:
+替换为-/替换为_- 去掉末尾的
=
这是因为 JWT 经常出现在 URL 参数或 HTTP 请求头中,必须使用 URL-safe 的编码。
5.3 JJWT 的模块化架构
JJWT 的模块化设计遵循接口与实现分离的原则:
- jjwt-api :定义公开 API,如
Jwts.builder()、JwtParserBuilder等 - jjwt-impl:提供具体实现,负责签名算法、时间验证等核心逻辑
- jjwt-jackson/jjwt-gson:JSON 序列化/反序列化插件
这种设计的优势在于:
- 可以独立升级实现模块而不影响业务代码
- 可以灵活选择 JSON 库,避免依赖冲突
六、JJWT vs 其他 Java JWT 库对比
Java 生态中有多个成熟的 JWT 库,以下是主流方案的对比:
| 特性 | JJWT | Auth0 java-jwt | Nimbus JOSE + JWT | jose4j |
|---|---|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 功能完整性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| JWE 支持 | ✅(0.12+) | ❌ | ✅ | ✅ |
| JWK 支持 | ✅(0.12+) | ❌ | ✅ | ✅ |
| 文档质量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 社区活跃度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
选型建议
- JJWT:API 最简洁直观,文档完善,适合大多数项目
- Nimbus JOSE + JWT:功能最全面,适合需要 JWE 加密、复杂 JWK 管理的高安全场景
- Auth0 java-jwt:轻量级选择,API 简单,但功能相对有限
- jose4j:功能强大,但 API 相对复杂
七、避坑指南
7.1 依赖配置陷阱
问题 :只引入 jjwt-api,运行时抛出 NoClassDefFoundError。
解决 :必须同时引入 jjwt-impl 和一个 JSON 序列化模块(jjwt-jackson 或 jjwt-gson)。
7.2 密钥强度不足
问题 :使用短字符串作为密钥,导致 WeakKeyException 或安全漏洞。
解决 :HMAC-SHA256 要求密钥长度 ≥ 256 位(32 字节)。使用 Keys.hmacShaKeyFor() 方法确保密钥强度。
java
// ❌ 错误:密钥太短
String weakKey = "abc123";
SecretKey key = Keys.hmacShaKeyFor(weakKey.getBytes());
// ✅ 正确:使用足够长的密钥
String strongKey = "your-256-bit-secret-key-here-at-least-32-characters";
SecretKey key = Keys.hmacShaKeyFor(strongKey.getBytes(StandardCharsets.UTF_8));
7.3 JDK 版本兼容性
问题 :在 JDK 9+ 中使用 JJWT 0.9.x,遇到 ClassNotFoundException: javax.xml.bind.DatatypeConverter。
解决:升级到 JJWT 0.12.x 版本,该版本不再依赖 JAXB。
7.4 签名不匹配
问题:生成和验证使用了不同的密钥。
解决:确保生成和验证使用完全相同的密钥(或对应的公私钥对)。
7.5 算法混淆攻击(alg=none)
问题 :服务端不校验 alg 字段,攻击者可将 alg 改为 none 绕过签名验证。
解决 :JJWT 0.12.x 默认不允许 none 算法。但应确保不使用已废弃的 Jwts.parser() 方法。
7.6 0.12.x 升级问题
问题:从 0.9.x 升级到 0.12.x 后大量编译错误。
主要变更:
setSubject()→subject()setIssuedAt()→issuedAt()setExpiration()→expiration()signWith(SignatureAlgorithm.HS256, secret)→signWith(SecretKey)setSigningKey()→verifyWith()
八、最佳实践
8.1 密钥管理
不要硬编码密钥:密钥应配置在环境变量或配置中心。
使用密钥轮换:定期更换密钥,缩短密钥暴露窗口。
生产环境密钥存储:使用 AWS KMS、HashiCorp Vault 等密钥管理服务。
8.2 Token 设计
设置合理的过期时间:Access Token 建议 15分钟到1小时。
使用标准声明 :尽量使用 iss、sub、aud、exp、iat 等标准字段。
最小化 Payload:只存放必要的身份标识信息,避免存放敏感数据。
8.3 安全实践
验证所有传入的 JWT:包括签名、过期时间、签发者、受众等。
使用强签名算法:优先选择 RS256、ES256 等非对称算法。
设置时钟偏移容忍度:允许一定的时间偏移(如 60 秒)以应对时钟不同步。
java
Jwts.parser()
.verifyWith(secretKey)
.clockSkewSeconds(60) // 允许 60 秒时钟偏移
.build();
8.4 Token 刷新与注销
刷新 Token 机制:使用 Refresh Token 获取新的 Access Token。
黑名单机制:在 Redis 或数据库中维护已撤销 Token 列表。
登出处理:将 Token 加入黑名单,并撤销对应的 Refresh Token。
8.5 异常处理
区分异常类型 :对 ExpiredJwtException、SignatureException、MalformedJwtException 等分别处理,返回不同的错误信息。
九、面试考点及解析
9.1 JWT 是什么?它的结构是怎样的?
参考答案 :JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以 JSON 对象的形式安全地传输信息。它由三部分组成:Header(头部)、Payload(负载)和 Signature(签名),三者通过 Base64Url 编码后用 . 连接。Header 包含令牌类型和签名算法;Payload 包含用户声明信息;Signature 用于验证令牌完整性和防止篡改。
9.2 JWT 为什么能实现无状态认证?
参考答案 :JWT 是自包含的,所有用户信息都编码在 Token 本身中。服务端接收到 Token 后,通过验证签名即可确认 Token 的真实性和完整性,无需在服务端存储任何会话信息。这使得服务端可以水平扩展,非常适合分布式和微服务架构。
9.3 JWT 和 Session 的区别是什么?
参考答案:
- 存储位置:Session 存储在服务端,JWT 存储在客户端
- 状态:Session 是有状态的,JWT 是无状态的
- 扩展性:Session 在分布式环境中需要共享存储,JWT 天然支持分布式
- 安全性:Session 可以随时撤销,JWT 在过期前无法主动失效
- 性能:Session 需要查询存储,JWT 只需验证签名
9.4 JWT 安全吗?有哪些风险?
参考答案:JWT 的安全性依赖于签名算法和密钥管理。主要风险包括:
- 密钥泄露:攻击者可伪造任意 Token
- 算法混淆 :
alg=none攻击(JJWT 0.12+ 已防护) - Payload 信息泄露:Payload 仅编码不加密
- Token 窃取:通过 XSS、中间人攻击等窃取 Token
- 无法主动失效:Token 在过期前无法撤销
防护措施:使用强密钥、HTTPS 传输、设置短过期时间、实现黑名单机制。
9.5 如何实现 JWT 的注销/登出?
参考答案:由于 JWT 是无状态的,无法主动使其失效。常用的方案有:
- 黑名单机制:在 Redis 或数据库中维护已注销 Token 列表,每次请求时检查
- 短过期时间 + Refresh Token:Access Token 短期有效,通过 Refresh Token 刷新
- 版本号机制:在 Payload 中加入版本号,用户注销时递增版本号
- 白名单机制:只在白名单中的 Token 才被认可
9.6 JJWT 0.12.x 相比 0.9.x 有哪些重大变化?
参考答案:
- 密钥处理 :强制使用
java.security.Key对象,不再支持字符串密钥 - API 重构 :
setSubject()→subject(),setExpiration()→expiration() - 解析器 API :
Jwts.parser()→Jwts.parser().verifyWith(key).build() - 模块化 :拆分为
jjwt-api、jjwt-impl、jjwt-jackson三个模块 - 新增功能:完整支持 JWE(加密 JWT)和 JWK(JSON Web Key)
9.7 HS256 和 RS256 有什么区别?如何选择?
参考答案:
- HS256(HMAC-SHA256) :对称加密,同一个密钥用于签名和验证。速度快,适合单服务或可信环境。
- RS256(RSA-SHA256) :非对称加密,私钥签名、公钥验证。适合微服务架构,不同服务只需持有公钥即可验证。
选择建议:内部服务间通信可用 HS256;对外 API 或微服务架构推荐 RS256。
十、总结
JJWT 作为 Java 生态中最流行的 JWT 实现库,经历了从 0.9.x 到 0.12.x 的重大演进,在安全性、类型安全和功能完整性上都得到了显著提升。
核心要点回顾:
-
理解 JWT 本质:JWT 是标准,JJWT 是实现。JWT 由 Header、Payload、Signature 三部分组成。
-
掌握 JJWT 用法 :0.12.x 版本强制使用 Key 对象,API 全面重构。正确配置
jjwt-api+jjwt-impl+jjwt-jackson三个依赖。 -
重视安全性:使用强密钥(≥32字节),不在 Payload 中存放敏感信息,选择适当的签名算法。
-
做好异常处理 :区分
ExpiredJwtException、SignatureException等不同异常,给出明确的错误反馈。 -
实践生产级方案:实现 Token 刷新、黑名单机制、密钥轮换等高级特性。
JJWT 0.12.x 已经不再是一个简单的"能跑就行"的签名库,而是一套可插拔、可审计、可灰度安全凭证生命周期管理框架。掌握 JJWT,不仅是掌握一个工具库的使用,更是深入理解现代 Web 安全认证体系的关键一步。