开放标准:JSON Web Token
前言
JSON Web Token (JWT ) 是一种开放标准 (RFC 7519 ),它定义了一种紧凑且自包含的方式,用于将信息作为 JSON 对象在各方之间安全地传输。此信息是经过数字签名的,因此可以验证和信任。可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对对 JWT 进行签名。
以下是 JSON Web Token 有用的一些情况:
- 授权 :这是使用 JWT 的最常见场景。用户登录后,每个后续请求都将包含 JWT ,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销小,并且能够轻松地跨不同域使用。
- 信息交换 :JSON Web Token 是在各方之间安全地传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确保发件人是他们所声称的身份。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
什么是JWT?
JSON Web Token 由三个部分组成,由点 ( . ) 分隔
xxxxx.yyyyy.zzzzz
具体来说,这三个部分是:
Header页眉 :标头通常由两部分组成:令牌的类型(JWT )和签名算法(HMAC SHA256 或 RSA)。
java
{
"alg": "HS256",
"typ": "JWT"
}
用Base64 对这个JSON 编码就得到JWT的第一部分。
-
Payload有效载荷 :令牌的第二部分是有效负载(JWT 中的主要数据,称为payload),其中包含声明。声明是关于实体(通常是用户)和其他数据的声明。 有三种类型的声明:registered , public 和 private。-
Registered claims :这些是一组预定义的声明,不是强制性的,但建议使用,以提供一组有用的、可互操作的声明。比如:
iss(签发者)、exp(过期时间)、sub(主题)、aud(接收者)、iat(签发时间)、nbf(在此之前不可用)等。 -
Public claims:这些声明可以由用户随意定义。但不建议添加敏感信息,因为该部分在客户端可解密。
-
Private claims:提供者和消费者所共同定义的声明,一般不建议存放敏感信息
-
一个示例有效Payload如下(并不需要三个声明都设置):
java
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
对Payload 进行Base64 编码就得到JWT的第二部分。
请注意,对于带签名的令牌来说,这些信息虽然得到了保护,不会被擅自篡改,但仍然可以被任何人读取。不要将机密信息放在 JWT 的
payload或header元素中,除非它们是加密的。
Signature签名 :要创建签名部分,您必须获取编码的Header 、编码的Payload、密钥、标头中指定的算法,并对其进行签名。
例如,如果您想使用 HMAC SHA256 算法,将按以下方式创建签名:
java
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
- Header页眉:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- Payload 有效载荷:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- 通过HMAC SHA256 算法得到:SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- 最后,我们将上述的 3 个部分的字符串通过
"."进行拼接得到完整JWT。

每当用户想要访问受保护的路由时,它都应该发送 JWT ,通常在 Authorization 标头中使用 Bearer 模式。因此,标头的内容应如下所示。
Authorization: Bearer < token >
在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查 Authorization 标头中的有效 JWT ,如果存在,将允许用户访问受保护的资源。跨域资源共享 (CORS ) 不会成为问题,因为它不使用 Cookie。
请注意,如果您通过 HTTP 标头发送 JWT 令牌,则应尽量防止它们变得太大。某些服务器不接受超过 8 KB 的标头。
下图显示了如何获取 JWT 并用于访问 API 或资源:

- 应用程序或客户端向授权服务器请求授权。
- 授予授权后,授权服务器将向应用程序返回访问令牌。
- 应用程序使用访问令牌访问受保护的资源(如 API)。
由于JSON 的表述方式比XML更简洁,因此在编码后,其体积也更小。
在大多数编程语言中,JSON 解析器都很常用,因为它们能够直接将数据映射为对象形式。而XML 则没有如此直接的"文档到对象"的转换方式。正因如此,JWT 成为在HTML 和HTTP环境中传输数据的首选格式。
在应用方面,JWT 被广泛用于互联网领域。这说明,客户端可以在多种平台上轻松处理JSON Web令牌,尤其是在移动设备上。
JWT验证
JSON Web Token (JWT)的验证对于保障安全性至关重要。
JWT 验证通常是对JWT的结构、格式和内容进行检查。
-
格式检查
- 确认有标准的三个组成部分:头部、有效载荷和签名,这三部分之间用点隔开。
- 每个部分都经过正确的编码处理(采用Base64URL格式),同时确保有效载荷中包含预期的数据内容。
- 检查有效载荷中的各项信息是否正确,比如有效期(
exp)、发行时间(iat)、"不得早于"的时间限制(nbf)等。这样可以确保令牌没有过期,也没有在应该使用之前就被提前使用了。
-
签名验证
- 这是验证过程中的关键步骤,其目的是检查JWT 中的签名部分是否与头部信息和有效载荷内容一致。这一过程使用头部中指定的算法来执行,常见的算法有HMAC 、RSA 或ECDSA等。验证时会用到密钥或公钥。如果签名与预期不符,那么很可能是该令牌被篡改过,或者它并非来自可信任的来源。
- 发行者验证:检查所声称的发行者是否与预期的发行者一致。
- 受众匹配检查:确保所宣称的受众群体与实际目标受众相符。
JWT编码与解码
编码时,需要将头部信息和有效载荷转换成一种紧凑的、适合在URL 中使用的格式。头部信息中包含了签名算法和令牌类型等信息;而有效载荷则包含主题、有效期、颁发时间等数据。这些信息都被转换为JSON 格式后,再使用Base64URL 编码。之后,将这些编码后的部分用点连接起来。最后,利用头部信息中指定的算法以及密钥来生成签名。这个签名同样也经过Base64URL 编码处理。这样,就得到了最终的JWT字符串,该字符串以一种便于传输或存储的形式表示了相应的令牌。
解码JWT 的过程其实是逆向操作 :将经过Base64URL 编码的头部信息和有效载荷转换回JSON 格式。这样一来,任何人都可以无需密钥就能读取这些信息。不过,在这里,"解码"一词通常还包括对令牌签名的验证。这一验证步骤涉及使用与最初生成令牌时相同的算法和密钥,重新生成解码后的头部信息和有效载荷的签名,然后将新生成的签名与JWT中的原始签名进行比较。如果两者一致,那就说明该令牌是完整且真实的,没有被人篡改过。
基本使用
Java 后端最常用的两个库是 JJWT 和 Auth0 java-jwt
JJWT
引入依赖:
xml
<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>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
该依赖包含了 jjwt-api、jjwt-impl 和 jjwt-jackson 三个模块的所有功能。
JJWT 保证其所有构件都符合语义化版本控制规范,但
jjwt-impl.jar除外。对于jjwt-impl.jar,不提供此类保证,该 JAR 内部的更改随时都可能发生。切勿将jjwt-impl.jar以compile范围添加到您的项目中,始终应将其声明为runtime范围。
创建JWT
调用Jwts.builder()方法创建一个 JwtBuilder 实例,写入信息,示例如下:
java
public static void main(String[] args) {
// 生成签名密钥
SecretKey key = Jwts.SIG.HS256.key().build();
String jwt = Jwts.builder()
.header()
.keyId("aKeyId")
.and()
.subject("Bob")
//.content(aByteArray, "text/plain")
.signWith(key)
//.encryptWith(key, keyAlg, encryptionAlg)
.compact();
System.out.println(jwt);
/** Output:
* eyJraWQiOiJhS2V5SWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb2IifQ.ZJwFiG-SBsb_d3cfNn-I59iC8D6x89Bi1sLclQax7vU
*/
}
在这个例子中,设置header 参数;注册声明 sub(主题)将被设置为 Bob ;然后我们使用适合 HMAC-SHA-256 算法的密钥对 JWT 进行签名,调用 compact() 方法生成最终的紧凑型 JWT 字符串。
JWT
payload可能是 byte[] 内容(通过content)或 JSON 声明(例如subject,claims,等等),但不能同时是两者。数字签名(
signWith)或加密(encryptWith)可使用其一,但不能同时使用。
JWT Header
JWT 头是一个 JSON 对象,它提供了关于 JWT 内容、格式以及任何相关加密操作的元数据。JJWT 提供多种方式来设置整个头信息以及/或多个单独的头参数(名称/值对)。
java
public static void main(String[] args) {
URI certUrl = URI.create("https://your-domain.com/certs/my-jwt-signer.crt");
String jwt = Jwts.builder()
.header()
.keyId("aKeyId")
.type("JWT")
.x509Url(certUrl)
.add("someName", "someValue")
.delete("someName")
.and()
.subject("Joe") // resume JwtBuilder calls...
.compact();
/** Output:
* eyJraWQiOiJhS2V5SWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb2IifQ.ZJwFiG-SBsb_d3cfNn-I59iC8D6x89Bi1sLclQax7vU
*/
}
keyId():设置密钥 ID。多密钥场景下,指定签名所用密钥的标识,方便验签方匹配公钥。type():设置令牌类型。默认自动设为 JWT,极少手动修改。x509Url():声明一个 URL 地址,验证方可以从该地址下载对应的 X.509 公钥证书(或证书链),并用它来验证 JWT 的签名。add():持任意名称/值对:也支持Map对象。delete():删除 Header 里指定的某一个字段。and():是 JJWT 链式构建器里的返回上级方法,专门用来结束子配置(Header /Payload),回到主构建器,让你能流畅地写链式代码。
大多数情况来说,除了自定义的Header 和默认的Header,其它方法很少调用。
自动设置头部:您无需设置
alg、enc或zip头部 - JJWT 会根据需要自动设置。
Jwts.header() 和 Jwts.builder().header() 之间只有两个区别:
Jwts.header() 构建一个 '分离的' Header ,它与任何特定的 JWT 都没有关联(没有and()方法),而 Jwts.builder().header() 总是修改其父 JwtBuilder 正在构建的 JWT 的头部。
独立的头部可能很有用,如果您想将常见的头部参数聚合到一个单个的 '模板' 实例中,这样您就不必为每个 JwtBuilder 使用重复它们。
java
public static void main(String[] args) {
Header commonHeaders = Jwts.header().add("someName", "someValue")
.build();
String jwt = Jwts.builder()
.header()
.add(commonHeaders) // <----
.add("specificHeader", "specificValue") // jwt-specific headers...
.and()
.subject("whatever")
// ... etc ...
.compact();
}
JWT Payload
一个 JWT payload 可以是任何东西 - 任何可以表示为字节数组的对象,例如文本、图像、文档等等。但既然 JWT header 始终是 JSON ,那么 payload 也可以是 JSON,特别是用于表示身份声明时。
JwtBuilder 支持两种不同的有效载荷选项:
content:如果您希望负载为任意字节数组内容。
java
public static void main(String[] args) {
String jwt = Jwts.builder()
.content("这是我的原始内容,不是JSON")
//.content("自定义内容".getBytes(StandardCharsets.UTF_8))
//.content("content", "text/plain") 参数一:有效载荷的实际字节内容 参数二:媒体类型的字符串标识符,自动设置 cty (内容类型)头部
.compact();
}
claims:如果您希望负载为 JSON 声明 Object。
java
public static void main(String[] args) {
String jwt = Jwts.builder()
.subject("Bob")
.issuer("me")
.audience().add("you").and()
.expiration(new Date())
.notBefore(new Date(System.currentTimeMillis()+1000))
.issuedAt(new Date())
.id(UUID.randomUUID().toString())
.claim("name", "张三")
.compact();
}
(1)subject():主题,一般放用户 ID。
(2)issuer() :签发者,谁发的 Token。
(3)audience() :接收者,谁能用 Token。
(4)expiration():过期时间,过了这个时间就不能用。
(5)notBefore():生效时间,没到这个时间,不能用。
(6)issuedAt() :签发时间,记录Token什么时候创建。
(7)id() :给这个 Token 一个唯一编号。
(8)claim():自定义声明
JWT 标准(RFC7519)原文规定:所有注册声明都是 OPTIONAL(可选的)
JWT Signed(JWS)
JWT 规范提供了对 JWT 进行加密签名的功能。对 JWT 进行签名,保证在 JWT 创建后,没有人对其进行篡改或更改(其完整性得到保持)。
java
public static void main(String[] args) {
// HS256 密钥(对称加密)
SecretKey key = Jwts.SIG.HS256.key().build();
String jwt = Jwts.builder()
.subject("1001")
.expiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.signWith(key) //签名
.compact();
}
调用 compact() 方法进行压缩和签名,生成最终的 JWS (JWT 是统称;JWS 是 带签名的、最安全、最常用的 JWT)。
JWT 规范确定了 13 种标准签名算法------3 种对称密钥算法和 10 种非对称密钥算法,这些都在
io.jsonwebtoken.Jwts.SIG注册类中以常量形式表示。
JWS 压缩
如果您的 JWT 负载很大(包含大量数据),并且您确信 JJWT 也将是读取/解析您的 JWS 的库,那么您可能希望压缩 JWS 以减小其大小。
JWS 不符合标准:JJWT 支持对 JWS 进行压缩,但这不是 JWS 的标准功能。JWT RFC 规范仅将此标准化为 JWE ,并且其他 JWT 库可能不会支持 JWS 压缩。只有当您确信 JJWT (或支持 JWS 压缩的另一个库)将解析 JWS 时,才使用 JWS 压缩。
java
public static void main(String[] args) {
// HS256 密钥(对称加密)
SecretKey key = Jwts.SIG.HS256.key().build();
String jwt = Jwts.builder()
.subject("1001")
.expiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.compressWith(Jwts.ZIP.DEF) // 压缩,DEFLATE
//.compressWith(Jwts.ZIP.GZIP) // 更高压缩率(非标准,跨系统需统一)
.signWith(key) //签名
.compact();
System.out.println(jwt);
}
必须 先压缩 Payload,再签名(仅 Payload 压缩)。若先签名再压缩,会篡改签名内容,导致验签失败。
无需手动解压,JJWT 自动识别 zip Header 并处理
读取JWT
使用 Jwts.parser() 方法创建一个 JwtParserBuilder 实例。
如果您期望解析已签名或加密的 JWT ,可以选择调用 keyLocator 、 verifyWith 或 decryptWith 方法。
如果解析、签名验证或解密失败,请将 parse* 调用包裹在 try/catch 块中。
java
public static void main(String[] args) {
SecretKey key = Jwts.SIG.HS256.key().build();
try {
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb2IiLCJpc3MiOiJtZSIsImF1ZCI6WyJ5b3UiXSwiZXhwIjoxNzc1OTgwMTk5LCJuYmYiOjE3NzU5ODAyMDAsImlhdCI6MTc3NTk4MDE5OSwianRpIjoiYjE1MzUyZDEtNzUwNS00MTU5LTlhZDMtZGU3NWNlZjBmMTI1IiwibmFtZSI6IuW8oOS4iSJ9.x2LZKFQgzNcB2dwGfpkov4a0n69tKsZ9P5z4t_x2Yro";
Jws<Claims> claimsJws = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt);
// 读取 Header
JwsHeader header = claimsJws.getHeader();
String keyId = header.getKeyId();
String type = header.getType();
String algorithm = header.getAlgorithm();
// 读取 Payload
Claims payload = claimsJws.getPayload();
// 标准字段
String sub = payload.getSubject(); // 用户ID
String jti = payload.getId(); // 令牌唯一ID
Date iat = payload.getIssuedAt(); // 签发时间
Date exp = payload.getExpiration(); // 过期时间
Date nbf = payload.getNotBefore(); // 生效时间
String iss = payload.getIssuer(); // 签发者
Set<String> aud = payload.getAudience(); // 接收方
// 自定义字段
String username = payload.get("username", String.class);
System.out.println("JWT is valid");
} catch (JwtException e) {
//don't trust the JWT!
System.out.println("JWT is invalid!");
}
}
JJWT 提供好几个 parse:
parseSignedClaims():带签名的 JWT(最常用)。
java
Jws<Claims> claimsJws = Jwts.parser().build().parseSignedClaims(jwt);
parseEncryptedClaims():解析加密 JWT(JWE)。
java
Jws<Claims> claimsJws = Jwts.parser().build().parseEncryptedClaims(jwt);
parseUnsecuredClaims():只解析 Header + Payload,不校验签名。
java
Jwt<Header, Claims> claimsJws = Jwts.parser().build().parseUnsecuredClaims(jwt);
如果您期望一个带有内容 payload 的 JWS ,请调用 JwtParser 的 parseSignedContent 方法。
java
Jws<byte[]> claimsJws = Jwts.parser().verifyWith(key).build().parseSignedContent(jwt);
byte[] contentBytes = claimsJws.getPayload();
String content = new String(contentBytes);
除此之外也支持内容的解密和不校验签名,获取Header + Payload。
java
// 解密
Jwe<byte[]> claimsJws = Jwts.parser().verifyWith(key).build().parseEncryptedContent(jwt);
//获取Header、Payload
Jwt<Header, byte[]> headerJwt = Jwts.parser().build().parseUnsecuredContent(jwt);
验证 JWT
调用Jwts.parser()方法,JJWT 内部检查这个 Token 是不是合法、有效、没被篡改、没过期。
java
public static void main(String[] args) {
String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux74";
boolean equals = Jwts.parser().verifyWith(key).build().parseSignedClaims(jws).getPayload().getSubject().equals("Joe");
System.out.println(equals);
/** Output:
* true
*/
}
如果验证失败,会抛出一个异常。
Exception in thread "main" io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
at io.jsonwebtoken.impl.DefaultJwtParser.verifySignature(DefaultJwtParser.java:340)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:576)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:364)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94)
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:36)
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:29)
at io.jsonwebtoken.impl.DefaultJwtParser.parseSignedClaims(DefaultJwtParser.java:827)
但如果解析或签名验证失败了怎么办?你可以及时发现并做出相应反应:
java
public static void main(String[] args) {
try {
// 生成签名密钥
SecretKey key = Jwts.SIG.HS256.key().build();
String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux75";
boolean equals = Jwts.parser().verifyWith(key).build().parseSignedClaims(jws).getPayload().getSubject().equals("Joe");
System.out.println(equals);
//OK, we can trust this JWT
System.out.println("JWT is valid");
} catch (JwtException e) {
//don't trust the JWT!
System.out.println("JWT is invalid!");
}
/** Output:
* JWT is invalid!
*/
}
常见验证失败异常:
- MalformedJwtException:格式错误。
java
public static void main(String[] args) {
// 生成签名密钥
SecretKey key = Jwts.SIG.HS256.key().build();
String jws = "eyJhbGciOiJIUzI1NiJ9.";
Jws<Claims> jwt= Jwts.parser().verifyWith(key).build().parseSignedClaims(jws);
}
- UnsupportedJwtException :不支持的JWT 异常,使用了 JWK + 不支持的密钥类型。
java
public static void main(String[] args) {
// 生成签名密钥
SecretKey key = Jwts.SIG.HS256.key().build();
String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux75";
Jws<Claims> jwt= Jwts.parser().build().parseSignedClaims(jws); // 未调用verifyWith方法进行验证
}
- SignatureException:签名错误,密钥不正确或者签名串不一致。
java
public static void main(String[] args) {
// 生成签名密钥
SecretKey key = Jwts.SIG.HS256.key().build();
String jws = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.vDv3eouyPSGC-t9g21XDgPtKIOvxPg91xIvaV_Tux75";
System.out.println(jws);
Jws<Claims> jwt= Jwts.parser().verifyWith(key).build().parseSignedClaims(jws); // 重新生成的key进行验证
}
- ExpiredJwtException:签名过期,过期时间小于当前时间。
javascript
public static void main(String[] args) {
// HS256 密钥(对称加密)
SecretKey key = Jwts.SIG.HS256.key().build();
String jwt = Jwts.builder()
.subject("1001")
.expiration(new Date(System.currentTimeMillis() - 1000)) // 设置当前时间之前
.signWith(key) //签名
.compact();
Jws<Claims> jws = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt);
}
- PrematureJwtException:签名过期,当前时间小于设置未来时间。
java
public static void main(String[] args) {
// HS256 密钥(对称加密)
SecretKey key = Jwts.SIG.HS256.key().build();
String jwt = Jwts.builder()
.notBefore(new Date(System.currentTimeMillis() + 100000))
.signWith(key) //签名
.compact();
Jws<Claims> jws = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt);
}
- IncorrectClaimException :声明异常。比如:
issuer、audience等不匹配(如果设置了就必须要校验,验证时不调用不会校验就毫无意义)。
java
public static void main(String[] args) {
// HS256 密钥(对称加密)
SecretKey key = Jwts.SIG.HS256.key().build();
String jwt = Jwts.builder()
.subject("Bob")
.signWith(key) //签名
.compact();
Jws<Claims> jws = Jwts.parser().verifyWith(key)
.requireId("123456") // 验证id不为空且匹配
.requireIssuer("issuer") // 验证issuer不为空且匹配
.requireAudience("audience") // 验证audience不为空且匹配
.requireSubject("subject") // 验证subject不为空且匹配
.build()
.parseSignedClaims(jwt);
}
JWE
JWT 规范还提供了加密和解密 JWT 的能力。保证除了预期的 JWT 接收者之外,没有人能看到 JWT payload (它是机密的),并且保证在 JWT 创建后,没有人对其进行篡改或更改(其完整性得到保持)。
JWT 规范定义了 6 种标准的认证加密算法,用于加密 JWT 。这些都作为常量在
io.jsonwebtoken.Jwts.ENC注册单例中表示,作为io.jsonwebtoken.security.AeadAlgorithm接口的实现。
java
public static void main(String[] args) {
// 生成签名密钥
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jwt = Jwts.builder()
.subject("1001")
.expiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.encryptWith(secretKey , Jwts.ENC.A128CBC_HS256) // 加密
.compact();
/** Output:
* eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..h4xwPcC1-RRLYeXpNOfluA.0Lf6C-SLHv4Dzmh91lCWuY_k3vdnvry-MTwNFpIXvgM.JhOJKtxV9MfZ71_CpuUbyg
*/
// 解析 JWE 令牌
try {
Claims payload = Jwts.parser()
.decryptWith(secretKey)
.build()
.parseEncryptedClaims(jwt)
.getPayload();
System.out.println("Decrypted Payload: " + payload.getSubject());
} catch (Exception e) {
System.err.println("Failed to decrypt JWE: " + e.getMessage());
}
}
大多数情况下,不会使用JWE ,一方面:再登录Token 中不允许放敏感信息;另一方面:JWE再加解密的过程比较消耗性能。
JWK
JSON Web 密钥(JWKs )是加密密钥的 JSON 序列化,允许将密钥材料嵌入 JWT 或以标准 JSON 基于文本格式在各方之间传输。它们本质上是一种基于 JSON 的替代其他基于文本的密钥格式,例如 DER 、PEM 和 PKCS12 文本字符串或文件,这些格式通常用于配置 Web 服务器上的 TLS。
简单来说:就是用 JSON 格式来表示一个密钥(公钥 / 私钥 / 对称密钥)
java
public static void main(String[] args) {
SecretKey key = Jwts.SIG.HS256.key().build();
SecretJwk jwk = Jwks.builder().key(key).idFromThumbprint().build();
/** Output:
* {alg=HS256, kty=oct, k=<redacted>, kid=zRAW7muU4kX00ChWJugUSJPA5YoqTaH0lLCVECu-f5Y}
*/
}
你可以通过构建 JwkParser 并使用其 parse 方法解析 JWK JSON 字符串来读取/解析 JWK:
java
public static void main(String[] args) {
String json = getJwkJsonString();
Jwk<?> jwk = Jwks.parser()
.build()
.parse(json); // 实际上解析 JSON 数据
Key key = jwk.toKey(); // 转换为 Java Key 实例
}
工具类
创建一个JWT工具类,方便使用:
java
public class JwtUtil {
// 生成方式:Jwts.SIG.HS256.key().build() → base64
private static final String SECRET_KEY_BASE64 = "你自己生成的32字节base64密钥";
// 过期时间:1天
private static final long EXPIRATION = 1000 * 60 * 60 * 24;
// 获取密钥对象
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(java.util.Base64.getDecoder().decode(SECRET_KEY_BASE64));
}
/**
* 生成 JWT Token
*/
public String generateToken(String userId) {
return Jwts.builder()
.subject(userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(getSecretKey())
.compact();
}
/**
* 生成带自定义声明的 Token
*/
public String generateToken(String userId, Map<String, Object> claims) {
return Jwts.builder()
.subject(userId)
.claims(claims)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(getSecretKey())
.compact();
}
/**
* 解析 Token 获取 Claims
*/
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 获取用户ID
*/
public String getUserId(String token) {
return parseToken(token).getSubject();
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (ExpiredJwtException e) {
System.out.println("Token 已过期");
} catch (MalformedJwtException e) {
System.out.println("Token 格式错误");
} catch (SecurityException | SignatureException e) {
System.out.println("签名无效");
} catch (UnsupportedJwtException e) {
System.out.println("不支持的 Token 类型");
} catch (IllegalArgumentException e) {
System.out.println("Token 参数为空");
}
return false;
}
}
Auth0 java-jwt
引入java-jwt 依赖,对比JJWT引入的少:
xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
创建 JWT
要创建一个 JWT ,你需要使用 JWT.create() 方法,然后添加必要的声明(claims),最后使用你的密钥和算法进行签名。示例代码如下:
java
public class Test {
public static void main(String[] args) {
// 创建 JWT
String token = JWT.create()
// .withHeader(map) 自定义Header,可以传map或json
.withIssuer("auth0") // 发行人
.withSubject("1234567890") // 主题
.withAudience("app_audience") // 观众
.withIssuedAt(new Date()) // 发行时间
.withExpiresAt(new Date(System.currentTimeMillis() + 3600 * 1000)) // 过期时间(1小时后)
// .withPayload(map) 自定义payload,可以传map或json
// .withClaim("test", "test") 自定义payload,指定name和value
.sign(Algorithm.HMAC256("123345")); // 使用 HMAC256 算法和密钥进行签名,默认用该参数的加密类型当作Header
System.out.println(token);
/** Output
* eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
* .eyJpc3MiOiJhdXRoMCIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJhcHBfYXVkaWVuY2UiLCJpYXQiOjE3MzYxMjc0ODksImV4cCI6MTczNjEzMTA4OX0
* .O4VD5DOn77tzPUZabPhPhkFKW9vS31dTpTAOPmkRv2o
*/
}
}
然后去官网可以查看解密的数据,如图所示:

验证JWT
验证一个 JWT ,我们需要创建一个 JWTVerifier 实例,该实例定义了验证 JWT 时所需的条件(如算法和密钥、发行人、观众等)。然后,我们使用 verify() 方法对 JWT 进行验证,并返回一个 DecodedJWT 实例,该实例包含了 JWT 中的所有声明。
java
public class Test {
public static void main(String[] args) {
// 验证 JWT
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("123456"))
.build(); // 可重用验证器实例
DecodedJWT jwt = verifier.verify("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJpc3MiOiJhdXRoMCIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJhcHBfYXVkaWVuY2UiLCJpYXQiOjE3MzYxMjc0ODksImV4cCI6MTczNjEzMTA4OX0" +
".O4VD5DOn77tzPUZabPhPhkFKW9vS31dTpTAOPmkRv2o");
System.out.println("Verified Token: " + jwt);
System.out.println("Verified Token: " + jwt.getId());
System.out.println("Verified Token: " + jwt.getIssuer());
/** Output
* Verified Token: com.auth0.jwt.JWTDecoder@2aece37d
* Verified Token: null
* Verified Token: auth0
*/
}
}
如果验证过程中出现密钥不匹配或者token过期都会抛出异常,如图所示:

工具类
创建一个JWT工具类,方便使用:
java
public class JwtUtil {
// 生成方式:Jwts.SIG.HS256.key().build() → base64
private static final String SECRET_KEY_BASE64 = "你自己生成的32字节base64密钥";
// 过期时间:1天
private static final long EXPIRATION = 1000 * 60 * 60 * 24;
// 获取密钥对象
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(java.util.Base64.getDecoder().decode(SECRET_KEY_BASE64));
}
/**
* 生成 JWT Token
*/
public String generateToken(String userId) {
return JWT.create()
.withSubject(userId)
.withIssuedAt(new Date()) // 发行时间
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION))
.sign(Algorithm.HMAC256(SECRET_KEY_BASE64));
}
/**
* 生成带自定义声明的 Token
*/
public String generateToken(String userId, Map<String, Object> claims) {
return JWT.create()
.withSubject(userId)
.withIssuedAt(new Date()) // 发行时间
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION))
.withPayload(claims)
.sign(Algorithm.HMAC256(SECRET_KEY_BASE64));
}
/**
* 解析 Token 获取 Claims
*/
public DecodedJWT parseToken(String token) {
return JWT.require(Algorithm.HMAC256(SECRET_KEY_BASE64)).build().verify(token);
}
/**
* 获取用户ID
*/
public String getUserId(String token) {
return parseToken(token).getSubject();
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
System.out.println("Token 失效");
}
return false;
}
}
JJWT和Auth0对比
经过介绍也能知道JJWT 提供的功能非常多,Auth0就比较简洁。
Spring Boot 项目最推荐JJWT ,链式 builder ,和 Spring 风格统一。
JJWT 提供的功能非常多,Auth0比较简洁。
一般接入 Auth0 第三方登录才用 Auth0 ,除非你非常喜欢极简 API,不想引入多个依赖。