还在为 JWT 的创建、签名和解析头疼?试试 JJWT 这个宝藏 Java 库!它是 JVM 和 Android 端超易用的 JWT 实现工具,纯 Java 开发、贴合 RFC 规范,把 JWT 的复杂逻辑全封装,靠构建器模式的简洁接口,新手也能快速上手,IDE 自动补齐还能大幅提升开发效率

Quickstart 快速入门

<!-- JJWT 核心 API -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.13.0</version>
</dependency>
<!-- 运行时实现 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>
<!-- JSON 序列化(Jackson) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>

大多数的JWT的复杂性都被隐藏在便捷且易读的 , 基于构建器模式的流畅接口的背后,这种设计非常适合依赖于IDE的自动补齐功能来快速编写代码

简单的不像话,短短两行代码就为我们生生成了jws,

JWS 就是 “带签名的 JWT”,签名的作用是防止 Token 被篡改,保证数据完整性。

在这个案例中,我们做了以下这些事:

1.构建了一个JWT, 将其注册声明(Subject/ 主题) 设置为ZJK

2.使用HS256算法的密钥对这个JWT进行签名

3.将其压缩拼接成最终的字符串形式

现在让我们验证 JWT(你应该始终丢弃签名与预期不符的 JWT)

assert Jwts.parser().verifyWith(key).build().parseSignedClaims(jws).getPayload().getSubject().equals("ZJK");

这行代码做了两件关键事:

1.使用之前生成的key验证 JWT 的签名 —— 如果签名验证失败(比如密钥不对、Token 被篡改),会抛出SignatureException(它继承自JwtException);

2.签名验证通过后,解析 JWT 的载荷(Claims),并断言(assert)其中的subject字段值是ZJK。不得不说,这种 “一拳搞定” 的单行代码太香了!

但如果解析或签名验证失败了怎么办?你可以捕获JwtException并做相应处理:

try {
    Jwts.parser().verifyWith(key).build().parseSignedClaims(compactJws);
    // 验证通过,我们可以信任这个JWT
} catch (JwtException e) {
    // 验证失败,不要信任这个JWT!
}

既然我们已经快速体验了如何创建和解析 JWT,接下来让我们深入讲解 JJWT 的 API。

创建 JWT 的标准步骤

1.调用Jwts.builder()创建JwtBuilder势力, 构建JWT的核心入口

2.[可选] 设置JWT头部header的自定义参数

3.调用构建器的方法设置载荷payload内容, 要么是Json声明(claims, 比如subject),要么是字节数组(byte[]),二者只能选其一

4.[可选] 调用signWith()签名, 或者encryptWith()加密方法 , 二者只能选其一

5.调用compact()方法最终生成紧凑的JWT字符串

载荷二选一:JWT 的 Payload 只能是「JSON Claims(如 subject/exp 等声明)」或「byte [] 二进制内容(需指定媒体类型)」,不能同时设置;

安全操作二选一:只能对 JWT 做「签名(signWith)」或「加密(encryptWith)」,不能既签名又加密;

无保护 JWT 警告:如果既不调用signWith()也不调用encryptWith(),生成的是「无保护 JWT(Unprotected JWT)」—— 这类 JWT 没有任何安全保障(可被任意篡改),生产环境绝对不能用!如需安全保障,必须在调用compact()前完成签名 / 加密。

JWT Header

JWT PayLoad

Payload 是 JWT 的核心数据载体,JJWT 支持两种类型(二选一),不能同时设置(否则compact()抛异常)。

类型 1:任意二进制内容(小众场景)

适用于传输文本、图片、文档等二进制数据,推荐使用content(byte[], 媒体类型)方法

import java.nio.charset.StandardCharsets;

public class JwtPayloadContentDemo {
    public static void main(String[] args) {
        SecretKey key = Jwts.SIG.HS256.key().build();
        // 二进制内容(比如文本转字节)
        byte[] content = "Hello, JJWT!".getBytes(StandardCharsets.UTF_8);

        String jwt = Jwts.builder()
                // 设置二进制Payload + 媒体类型(IANA标准,如text/plain、application/json)
                .content(content, "text/plain")
                .signWith(key)
                .compact();

        System.out.println("二进制Payload的JWT:" + jwt);
    }
}
类型 2:JSON Claims(主流场景)

适用于身份认证、权限传递等场景,Payload 是 JSON 对象,支持标准 Claims自定义 Claims,是后端开发最常用的方式。

示例:完整配置标准 + 自定义 Claims

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class JwtPayloadClaimsDemo {
    public static void main(String[] args) {
        SecretKey key = Jwts.SIG.HS256.key().build();
        Date now = new Date();
        Date expiration = new Date(now.getTime() + 3600 * 1000); // 1小时过期
        Date notBefore = new Date(now.getTime() + 60 * 1000); // 1分钟后生效

        // 批量自定义Claims(Map)
        Map<String, Object> customClaims = new HashMap<>();
        customClaims.put("name", "John Doe");
        customClaims.put("admin", true);

        String jwt = Jwts.builder()
                // ===== 标准Claims(类型安全方法)=====
                .issuer("https://my-app.com") // iss:签发人
                .subject("1234567890")       // sub:用户ID
                .audience().add("web-admin").and() // aud:受众
                .expiration(expiration)      // exp:过期时间
                .notBefore(notBefore)        // nbf:生效时间
                .issuedAt(now)               // iat:签发时间
                .id(UUID.randomUUID().toString()) // jti:唯一ID

                // ===== 自定义Claims =====
                .claim("age", 30) // 单个自定义Claim
                .claims(customClaims) // 批量添加(Map)

                .signWith(key)
                .compact();

        System.out.println("JSON Claims的JWT:" + jwt);

        // 解析验证Claims
        Claims payload = Jwts.parser()
                .verifyWith(key)
                .build()
                .parseSignedClaims(jwt)
                .getPayload();

        // 读取标准Claims
        System.out.println("标准Claims - sub:" + payload.getSubject());
        // 读取自定义Claims
        System.out.println("自定义Claims - name:" + payload.get("name", String.class));
        System.out.println("自定义Claims - admin:" + payload.get("admin", Boolean.class));
    }
}

关键规则 & 最佳实践

  • 二选一约束content()claims()/subject()等不能同时用,否则抛异常;
  • 优先用标准方法:标准 Claims(如subject()/expiration())优先用专用方法,比claim("sub", "xxx")更易读;
  • 自定义 Claims 注意claim()会覆盖同名 Key(比如先claim("age",20)claim("age",30),最终是 30)。
1. Claims Map(声明映射)

如果希望一次性添加多个声明,可以使用JwtBuilderclaims(Map)方法:

Map<String,?> claims = getMyClaimsMap(); // 自行实现该方法
String jws = Jwts.builder()
 .claims(claims)
 // ... 其他配置 ...
2. JWT Compression(JWT 压缩)

如果你的 JWT 载荷(Payload)数据量较大(包含大量数据),你可能需要压缩 JWT 以减小其体积。请注意:这并非所有 JWT 的标准特性 —— 该特性仅针对 JWE(加密的 JWT)定义,且其他 JWT 库对于非 JWE 类型的令牌(token)可能不支持压缩。不过,JJWT 库同时支持对 JWS(签名的 JWT)和 JWE 进行压缩。

JWT Read(解析 JWT)

你可按如下步骤读取(解析)JWT:

  1. 调用 Jwts.parser() 方法创建 JwtParserBuilder 实例;
  2. 若预期要解析带签名或加密的 JWT,可选择性调用 keyLocatorverifyWithdecryptWith 方法;
  3. 调用 JwtParserBuilderbuild() 方法,创建并返回一个线程安全的 JwtParser 实例;
  4. 根据预期的 JWT 类型,传入紧凑格式的 JWT 字符串,调用各类 parse* 解析方法中的一种;
  5. parse* 方法的调用逻辑包裹在 try/catch 代码块中,以防解析、签名验证或解密过程失败。
Jwt<?,?> jwt;
try {
 jwt = Jwts.parser() // (1) 创建解析器构建器
 .keyLocator(keyLocator) // (2) 动态定位签名/加密密钥
 //.verifyWith(key) // 或使用固定密钥验证所有带签名的 JWT
 //.decryptWith(key) // 或使用固定密钥解密所有加密的 JWT
 .build() // (3) 构建解析器
 .parse(compact); // (4) 也可使用 parseSignedClaims、parseEncryptedClaims、parseSignedContent 等方法
 // 此时可安全信任该 JWT
} catch (JwtException ex) { // (5) 捕获异常
 // 我们**无法**按创建者的预期使用该 JWT
}

 注意:类型安全的 JWT若你确定解析器只会处理特定类型的 JWT

(例如,仅使用包含 Claims 载荷的带签名 JWT,或包含二进制内容载荷的加密 JWT 等),可调用对应的类型安全方法(如 parseSignedClaimsparseEncryptedClaims 等),

而非通用的 parse 方法。这些 parse* 方法会返回你预期的类型安全 JWT 对象,

例如 Jws<Claims>Jwe<byte[]>

而非通用的 Jwt<?,?> 实例。

固定解析密钥

若待解析的 JWT 是 JWS(带签名的 JWT)或 JWE(加密的 JWT),则必须使用密钥来验证签名或解密。如果是 JWS 且签名验证失败,或是 JWE 且解密失败,该 JWT 无法被安全信任,应直接丢弃。

那么我们该使用哪种密钥?

  • 若解析的是 JWS,且该 JWS 是用 SecretKey(对称密钥)签名的,则需在 JwtParserBuilder 上指定相同的 SecretKey。示例:
    Jwts.parser()
     .verifyWith(secretKey) // <---- 指定对称密钥
     .build()
     .parseSignedClaims(jwsString);
    
  • 若解析的是 JWS,且该 JWS 是用 PrivateKey(非对称私钥)签名的,则需在 JwtParserBuilder 上指定该密钥对应的 PublicKey(公钥),而非私钥。示例:
    Jwts.parser()
     .verifyWith(publicKey) // <---- 用公钥(而非私钥)
     .build()
     .parseSignedClaims(jwsString);
    
  • 若解析的是 JWE,且该 JWE 是通过直接加密方式、使用 SecretKey(对称密钥)加密的,则需在 JwtParserBuilder 上指定相同的 SecretKey。示例:
    Jwts.parser()
     .decryptWith(secretKey) // <---- 也可使用 Keys.password(charArray) 生成的密码密钥
     .build()
     .parseEncryptedClaims(jweString);
    
  • 若解析的是 JWE,且该 JWE 是通过密钥算法、使用 PublicKey(非对称公钥)加密的,则需在 JwtParserBuilder 上指定该密钥对应的 PrivateKey(私钥),而非公钥。示例:
    Jwts.parser()
     .decryptWith(privateKey) // <---- 用私钥(而非公钥)
     .build()
     .parseEncryptedClaims(jweString);

Sign 签名密钥

如果您不想考虑密钥的位长度要求,或是想简化开发流程,JJWT 提供了便捷的构建器类,可为您想要使用的任意 JWT 签名算法生成足够安全的密钥。


对称密钥(Secret Keys)

如果您希望生成一个足够安全的 SecretKey(对称密钥),用于 JWT 的 HMAC-SHA 系列算法(如 HS256/HS384/HS512),请使用对应算法的 key() 构建方法:

SecretKey key = Jwts.SIG.HS256.key().build(); // 也可使用 HS384.key() 或 HS512.key()

在底层实现中,JJWT 会使用 JCA(Java Cryptography Architecture)默认提供程序的 KeyGenerator,为指定算法生成符合最小长度要求的安全随机密钥(比如 HS256 会自动生成 256 位 / 32 字节密钥)。

若您希望在密钥生成时指定特定的 JCA 提供程序(Provider)或安全随机数生成器(SecureRandom),可将其作为构建器参数传入。例如:

SecretKey key = Jwts.SIG.HS256.key().provider(aProvider).random(aSecureRandom).build();

如果需要保存这个新生成的 SecretKey,您可以对其进行 Base64(或 Base64URL)编码:

String secretString = Encoders.BASE64.encode(key.getEncoded());

注意:务必将生成的 secretString 保存在安全位置 ——Base64 编码并非加密,因此该字符串仍属于敏感信息。例如,您可以在将其保存到磁盘前对其进行进一步加密处理。

  • Encoders.BASE64.encode(key.getEncoded()) 的核心是把二进制密钥转成可存储的字符串
  • 这么做的目的是解决二进制密钥无法配置、无法传输的问题;
  • 重点记住:Base64 编码不是加密,密钥字符串必须安全存储,使用时需要解码还原。

非对称密钥(Asymmetric Keys)

如果您希望生成足够安全的椭圆曲线(ECDSA)或 RSA 非对称密钥对,用于 JWT 的 ECDSA 或 RSA 系列算法(如 RS256/RS384/RS512/PS256 等),请使用对应算法的 keyPair() 构建方法:

KeyPair keyPair = Jwts.SIG.RS256.keyPair().build(); // 也可使用 RS384、RS512、PS256 等算法

生成 KeyPair(密钥对)后,您可以使用其中的私钥(keyPair.getPrivate())创建带签名的 JWS(即签名 JWT),并使用公钥(keyPair.getPublic())解析 / 验证该 JWS。

如何从Claims中拿到事先存好的数据呢?

核心前提:存入和取出的「key 必须一致」

首先明确:你往 JWT 里存数据时用的key(比如userId/username/role),就是取出时的唯一标识。

方式 1:指定类型取值(推荐,最安全)

Claims.get(String key, Class<T> type):直接指定要获取的数据类型,避免手动强转,类型不匹配时会抛清晰的异常,便于排查。

方式 2:基础取值(需手动强转,不推荐)

Claims.get(String key):返回Object类型,需要手动强转,类型不匹配时会抛ClassCastException(异常信息不清晰)。

方式 3:带默认值取值(避免空指针)

Claims.getOrDefault(String key, T defaultValue):如果 key 不存在,返回指定的默认值,适合非必填字段。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐