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

相关推荐
圆山猫7 小时前
[Virtualization](四):Linux KVM/RISC-V 的 vCPU 运行路径
java·linux·risc-v
笨鸟先飞,勤能补拙8 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
城管不管8 小时前
ReAct、Plan-and-Execute、Reflection 三大智能 Agent 范式核心区别
java·人工智能·算法·spring·ai·动态规划
IT小白杨8 小时前
从环境制备到自动化工作流:多账号运营的工程化架构拆解
java·经验分享·自动化·安全架构·指纹浏览器
长和信泰光伏储能8 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
豆瓣鸡9 小时前
算法日记 - Day3
java·开发语言·算法
浦信仿真大讲堂9 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
萧瑟余晖9 小时前
Java深入解析篇九之NIO详解
java·网络·nio
The Chosen One9859 小时前
高进度算法模板速记(待完善)
java·前端·算法