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

相关推荐
AIFQuant5 小时前
Python实时外汇行情接入实战:WebSocket与REST K线查询
开发语言·python·websocket
babe小鑫5 小时前
生物统计学专业校招:SAS、R、Python学习顺序实用指南
python·学习·r语言
vx_Biye_Design5 小时前
springboot游泳馆系统93765-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·spring·课程设计
名字还没想好☜6 小时前
Java NIO ByteBuffer 实战:flip/clear/compact 三个绕晕人的方法与 position/limit 心智模型
java·开发语言·后端·spring·nio
固定资产管理系统软件6 小时前
该去哪里找专业靠谱的智慧智能设备固定资产管理系统?
人工智能·python
Wang's Blog6 小时前
Java框架快速入门: Spring Security+OAuth2之短信服务多供应商动态切换(阿里云与LeanCloud)
java·spring·阿里云
Doubbbbbbble云6 小时前
区间合并问题的常见算法模式与优化思路4
java·数据结构·算法
ctlover6 小时前
LangChain 概述
python·langchain
小溪学编程6 小时前
AQS 原理详解:从 CLH 队列到 ReentrantLock 的实现
java·开发语言
专业程序开发源6 小时前
springbootLivehouse票务系统-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·课程设计·pygame