jjwt 0.9.1 在 JDK 11+ 上的两个“坑”与完整解决方案

环境: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 移除的运行时模块;从架构角度看,升级库才是正解。

相关推荐
王中阳Go12 小时前
两个入口答案不一致先别调模型:6 行入口口径对账表(附脚本)
人工智能·后端·python·大语言模型·1024程序员节
BK贝壳12 小时前
bool-hybrid-array 落盘我只认 .bha:用 Create_BHA / Ask_BHA 做布尔数组序列化
数据结构·python·numpy·1024程序员节
Sakura_j_h_13 小时前
Python 在定时任务里输出 emoji,UnicodeEncodeError 从哪来
开发语言·字符编码·windows·python·定时任务
thefool11226613 小时前
TCP 1
java·1024程序员节
supabc12314 小时前
欧盟电子发票(XRechnung/Peppol/ZUGFeRD)踩坑记:用 Python 不装 Java 跑官方校验规则
xml·python·开源·电子发票·1024程序员节
ZhengEnCi14 小时前
AI Agent工具调用防护层怎么写-一份能直接跑的Python四层拦截实现
python·llm·agent
ZhengEnCi15 小时前
AI Agent工具误调用怎么解决-从参数校验到高危二次确认的四层工程防线
python·llm·agent
金銀銅鐵15 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
AI人工智能+电脑小能手15 小时前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务
镜子AI16 小时前
镜子AI工程教程:用Python实现关键词布局检查器与语义改写查重(附完整代码)
人工智能·python·microsoft