环境:Spring Boot 3.5.16 / Java 21 / jjwt 0.9.1
场景:在单元测试中签发 JWT,连续踩中两个运行时异常。
一、背景
项目使用 jjwt(io.jsonwebtoken:jjwt)0.9.1 生成 JWT。一段最简单的签发代码:
java
String token = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "123456")
.addClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.compact();
在 Java 21 下运行,先后抛出两个错误。下面逐一拆解。
二、报错一:密钥算法与签名方式不匹配
异常信息:
java.lang.IllegalArgumentException:
Base64-encoded key bytes may only be specified for HMAC signatures.
If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.
at io.jsonwebtoken.impl.DefaultJwtBuilder.signWith(DefaultJwtBuilder.java:98)
原因:
jjwt 的 signWith(SignatureAlgorithm, String) 这个重载只对 HMAC 算法(HS256/HS384/HS512)有效------它会把字符串当作 base64 编码的密钥处理。
而 ES256(Elliptic Curve,椭圆曲线)和 RS256(RSA)属于非对称算法,只能通过 signWith(SignatureAlgorithm, Key) 传入 Key 对象,不能传字符串。
修复: 如果你只是想用字符串密钥,那本质上就是 HMAC 用法,把算法改成 HS256(并换成长度足够的密钥)即可:
java
// HS256 要求密钥至少 32 字节(base64 编码)
String secret = Base64.getEncoder()
.encodeToString("12345678901234567890123456789012".getBytes());
Jwts.builder().signWith(SignatureAlgorithm.HS256, secret) ...
如果你确实要用 ES256/RSA,需用
KeyPairGenerator生成密钥对,调用signWith(alg, privateKey)。
三、报错二:JDK 11+ 缺失 JAXB(真正的"硬骨头")
异常信息:
java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter
原因:
javax.xml.bind(JAXB)在 JDK 8 里是标准库的一部分;但从 JDK 11 起被正式移除 (JEP 320)。jjwt 0.9.1 的 Base64Codec 依赖 javax.xml.bind.DatatypeConverter 做所有 Base64 编解码------不仅是密钥解码,连 compact() 编码 JWT 的 header/payload 也会用到它。因此,只要在 JDK 11+ 上运行 0.9.1,就一定会在某一步触发这个 NoClassDefFoundError。
尝试过的"最小改动"及其失败原因
一开始我们尝试绕过它:
- 改
signWith(alg, byte[])重载 :用byte[]代替String,确实避开了密钥解码那一次DatatypeConverter调用。 - 结果 :仍然报错,只是报错位置后移到了
compact()→Base64UrlCodec.encode→Base64Codec.encode。
结论:jjwt 0.9.1 的全部 Base64 操作都走 JAXB,单靠改调用方式无法根治,必须把 JAXB 本身补回运行时。
四、解决方案
根据是否升级 jjwt,有两条路线。
方案 A:保留 0.9.1,补回 JAXB 依赖(改动最小)
在 pom.xml 中增加两个依赖,提供 DatatypeConverter 的实现:
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jjwt 0.9.1 依赖 JAXB 的 DatatypeConverter,JDK 11+ 已移除,需手动补回 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.1</version>
<scope>runtime</scope>
</dependency>
刷新 Maven 后测试即通过。jaxb-api 提供类定义,jaxb-impl 提供运行时实现,两者缺一不可。
最终可用的测试代码(保留 0.9.1 写法):
java
@Test
public void testGenerateToken() {
Map<String, Object> claims = new HashMap<>();
claims.put("username", "jinyong");
claims.put("password", "123456");
String token = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "YW5jaGFvMTIz")
.addClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.compact();
System.out.println(token);
}
方案 B:升级到 jjwt 0.11.x(推荐,根治)
0.11.x 重写了编解码层,不再依赖 JAXB,API 也更规范。依赖改为三件套:
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
0.11.x 的 signWith 参数顺序变了,且 HS256 需要 SecretKey:
java
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
SecretKey key = Keys.hmacShaKeyFor("12345678901234567890123456789012".getBytes());
String token = Jwts.builder()
.signWith(key, SignatureAlgorithm.HS256) // 注意:Key 在前
.addClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.compact();
校验侧也需同步:
Jwts.parser().setSigningKey(key).build().parseClaimsJws(token)(0.11.x 多了build())。
五、最佳实践小结
| 项 | 建议 |
|---|---|
| 算法与密钥匹配 | 字符串密钥 → 用 HMAC(HS256);非对称 → 用 Key 对象 |
| 密钥长度 | HS256 至少 32 字节,不要用 "123456" 这种短串做生产密钥 |
| JDK 11+ 用 jjwt 0.9.1 | 必须补 jaxb-api + jaxb-impl,否则 NoClassDefFoundError |
| 治本方案 | 升级到 jjwt 0.11.x,彻底摆脱 JAXB 依赖 |
核心认知 :NoClassDefFoundError: javax.xml.bind.DatatypeConverter 不是代码写错,而是"老库 + 新 JDK"的典型组合问题。改调用方式只能推迟报错,真正要补的是被 JDK 移除的运行时模块;从架构角度看,升级库才是正解。