跳到主要内容

JWT学习

概述

官方文档

JWT 即 JSON Web Token,通过 JSON 形式作为 Web 应用中的令牌,用于在各方安全的将信息作为 JSON 对象传输,在传输过程中还可以完成数据加密、签名等操作(就是可以做数据交换或者安全验证)

例如用来维持登陆状态,传统的形式是通过 Session,但是使用 Session 有诸多麻烦(具体看过往文章),所以可以使用 JWT 来把维持状态放在客户端,服务端只需取得该 JWT 即可判断当前用户的状态

认证流程

用法和之前写的报修系统后台是一样的,首先前端通过 Web 表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个 HTTP 的 POST 请求,后端核对用户名和密码成功后,将用户的 id 等其他信息作为 JWT Payload(负载),将其与头部分别进行 Base64 编码拼接后签名,形成的 JWT 就是一个形同 lll.zzz.xxx 的字符串

后端将 JWT 字符串返回给前端,前端把这个 JWT 存到 localStorage 上,以后前端每次发送请求时把这个 JWT 放到 Header 中的 Authorization 里面发送(全局)

令牌组成

参考资料 什么是 JWT -- JSON WEB TOKEN 参考资料 JWT 到底应该怎么用才对?

标头:Header 有效载荷:Payload 签名:Signature

所以结构为:Header.Payload.Signature

标头通常由两部分组成:令牌类型(JWT)和所使用的签名算法,例如 HMAC、SHA256 或 RSA。它会使用 Base64 编码组成 JWT 结构的第一部分

{
"alg" : "HS256",
"typ" : "JWT"
}

Payload

令牌的第二部分是有效荷载,主要携带用户的数据信息,使用的也是 Base64 编码(所以不要在这里存放敏感信息);载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用):

  • iss: jwt签发者
  • sub: jwt所面向的用户(也可理解为面向用户的类型)
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共字段和私有字段都是用户可以任意添加的字段,区别在于公共字段是一些约定俗成,被普遍使用的字段,而私有字段更符合实际的应用场景。

当前已有的公共字段可以从 JSON Web Token Claims 中找到。

{
"sub": "1234567890",
"id" : "123456",
"name" : "john",
"admin" : "true",
"iat": 1516239022
}

Signature

前两部分都是使用 Base64 进行编码的,即前端可以解开指定里面的信息,而最后这个 Signature 需要使用编码后的 header 和 payload 以及自己提供的一个密钥进行签名(Header 指定的算法),签名的作用是确保 JWT 没有被篡改

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);

服务端会对返回的数据的这个 Signature 与前面的数据重新加密,如果重新加密的结果是一样的则放行,如果不一样则丢弃

注:Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2 的 6 次方等于 64,所以每 6 比特为一个单元,对应某个可打印字符。三个字节有 24 比特,对应于 4 个 Base64 单元,即 3 个字节需要 4 个可打印字符来表示.JDK 中提供了非常方便的 BASE64EncoderBASE64Decoder,直接使用它们就能完成编程和解码

使用 auth0-JWT

引入环境

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>

生成 JWT

@Test
void contextLoads() {
// 创建一个 日历 对象
// 用法
// //加3年
// calendar.add(Calendar.YEAR, 3);
// //加2月
// calendar.add(Calendar.MONTH, 2);
// //减30天,对天的加减只用DAY_OF_YEAR
// calendar.add(Calendar.DAY_OF_YEAR, -30);

Calendar instance = Calendar.getInstance();
// 表示偏移 90 秒
instance.add(Calendar.SECOND, 90);
String token = JWT.create()// header 可以省略,因为有默认值
.withClaim("userId", "1234") // payload
.withClaim("username", "john")
.withExpiresAt(instance.getTime()) // 设置超时时间
.sign(Algorithm.HMAC256("this_is_secret_key")); // 设置密钥

System.out.println(token);
}

生成的 JWT 如下

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM3NjgxOTIsInVzZXJJZCI6IjEyMzQiLCJ1c2VybmFtZSI6ImpvaG4ifQ.Nhx3Rsjx0tQ6Zg8oqs7v7T_UFgwBoqmS_j9ht6qcmsk

解析 JWT

@Test
void test() {
// 密钥要一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("this_is_secret_key")).build();
String token =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJleHAiOjE2MDM3Njk2NTQsInVzZXJJZCI6IjEyMzQiLCJ1c2VybmFtZSI6ImpvaG4ifQ." +
"j4r05XEWoobgbBt6aJ47HVB2-bcq2VdQWR8Go17H7WU";
DecodedJWT decodedJWT = jwtVerifier.verify(token);
// 注意:如果有多个 Claim 时要通过 getClaims 来取
// 且,这里要取得参数要使用对应类型的 例如 String 就是 asString、Int 类型就是 asInt
System.out.println("用户Id:" + decodedJWT.getClaims().get("userId").asString());
System.out.println("用户名:" + decodedJWT.getClaims().get("username").asString());
System.out.println("过期时间:" + decodedJWT.getExpiresAt());
}

常见异常:

  • TokenExpiredException:令牌过期
  • SignatureVerificationException:签名不一致异常
  • AlgorithmMismatchException:算法不一致异常
  • InvalidClaimException:失效的 payload 异常

封装成工具类

public class JWTUtils {
// 密钥
private static final String SIGN = "this_is_secret_key";
// 过期时间 单位为秒
private static final int EXPIRATION = 1800;

// 生成 token
public static String getToken(Map<String, String> map) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, EXPIRATION);

// 创建 JWT Builder
JWTCreator.Builder builder = JWT.create();

// 把传入的内容添加到 payload 里面去
map.forEach(builder::withClaim);
// 等价于
// map.forEach((k,v)->{
// builder.withClaim(k,v);
// });

return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SIGN));
}

// 验证 token 获取信息
public static DecodedJWT verifier(String token) {
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
}

集成到 SpringBoot

JWT 的工具类基本同上不用变,主要是添加一个拦截器

/**
* JWT过滤器,拦截 /secure的请求
*/
@Slf4j
@WebFilter(filterName = "JwtFilter", urlPatterns = "/secure/*")
public class JwtFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;

response.setCharacterEncoding("UTF-8");
//获取 header里的token
final String token = request.getHeader("authorization");

if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
chain.doFilter(request, response);
}
// Except OPTIONS, other request should be checked by JWT
else {

if (token == null) {
response.getWriter().write("没有token!");
return;
}

Map<String, Claim> userData = JwtUtil.verifyToken(token);
if (userData == null) {
response.getWriter().write("token不合法!");
return;
}
Integer id = userData.get("id").asInt();
String name = userData.get("name").asString();
String userName = userData.get("userName").asString();
//拦截器 拿到用户信息,放到request中
request.setAttribute("id", id);
request.setAttribute("name", name);
request.setAttribute("userName", userName);
chain.doFilter(req, res);
}
}

@Override
public void destroy() {
}
}

使用 JJWT

参考资料 JJWT 官方文档 参考资料 Java Web Token 之 JJWT 使用

上面的那个 auth0 好像不是很常用,这里补个 JJWT 的用法

添加依赖

<!-- 配置参数 -->
<properties>
<java.version>1.8</java.version>
<jwt.version>0.10.7</jwt.version>
</properties>



<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>

构建 JWT

@Test
public void getJWTTest() {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jws = getJwtStr(key);
if (log.isDebugEnabled()) {
log.debug(jws);
}
}

private String getJwtStr(Key key) {
return Jwts.builder()
.setSubject("JDKONG")
.signWith(key)
.compact();
}

在以上代码中,构建的过程如下:

  • 构建一个主题为 JDKONG 的 JWT;
  • 使用适用于 HMAC-SHA-256 算法的密钥对 JWT 进行签名;
  • 最后,将它压缩成最终的 String 形式。 签名的 JWT 称为 JWS。

最终生成的 JWT 如下所示:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKREtPTkcifQ.C-eSTnoK-lryYVerB6SCbgbTRMKpXyWvDJNNPH07g3Q

解析 JWT

现在,通过类似的方式验证 JWT:

@Test
public void parseJwtStr() {
// 得到密钥
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 得到 JWT
String jwtStr = getJwtStr(key);
// 验证 JWT
assert Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody()
.getSubject()
.equals("JDKONG");
}

这里需要注意两件事:

  • 之前的密钥用于验证 JWT 的签名。 如果它无法验证 JWT,则抛出 SignatureException(从JwtException扩展)。
  • 如果 JWT 已经过验,会接着断言该 claim 设置为 JDKONG。如果都没问题,则验证通过。

如果,在验证的过程中失败了会怎样呢?在做 JWT 解析时,可以捕捉异常 JwtException ,比如:

try {
Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);
//OK, we can trust this JWT
} catch (JwtException e) {
//don't trust the JWT!
}

JWT 加密签名实现

JWT 本身是支持加密签名的,在使用签名的 JWT 时,需要注意一下两点:

  • 保证 JWT 是由我们认识的人(它是真实的)创建的
  • 保证在创建 JWT 之后没有人操纵或改变JWT(保持其完整性)。

真实性和完整性 保证 JWT 包含可以信任的信息。 如果 JWT 未通过真实性或完整性检查,应该始终拒绝 JWT,因为我们无法信任它。

那么 JWT 如何签约? 让我们通过一些易于阅读的伪代码来完成它:

假设我们有一个带有 JSON 头和主体的 JWT:

String header = '{"alg":"HS256"}'
String claims = '{"sub":"JDKONG"}'

对他们分别进行UTF_8编码:

String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )

将编码后的 Header 和 Body 使用 . 进行分隔,并连接成一个字符串:

String concatenated = encodedHeader + '.' + encodedClaims

使用加密秘密或私钥,选择的签名算法(此处使用HMAC-SHA-256),并对连接的字符串进行签名:

Key key = getMySecretKey()
byte[] signature = hmacSha256( concatenated, key )

由于签名始终结果是字节数组,因此 Base64URL 对签名进行编码并使用 . 将 它连接到字符串 concatenated 后面:

String jws = concatenated + '.' + base64URLEncode( signature )

最后生成的结果:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4

这被称为JWS - 签名JWT的简称。

JWT 签名算法介绍

支持算法类型

JWT规范确定了12种标准签名算法–3种密钥算法和9种非对称密钥算法 - 由以下名称标识:

HS256: HMAC using SHA-256 HS384: HMAC using SHA-384 HS512: HMAC using SHA-512 ES256: ECDSA using P-256 and SHA-256 ES384: ECDSA using P-384 and SHA-384 ES512: ECDSA using P-521 and SHA-512 RS256: RSASSA-PKCS-v1_5 using SHA-256 RS384: RSASSA-PKCS-v1_5 using SHA-384 RS512: RSASSA-PKCS-v1_5 using SHA-512 PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512

这些都在 io.jsonwebtoken.SignatureAlgorithm 枚举类中表示。

除了它们的安全属性之外,这些算法真正重要的是JWT规范 RFC 7518第3.2到3.5节 强制要求必须使用对所选算法足够强大的密钥。

这意味着JJWT也会强制使用足够强的密钥。 如果为给定算法提 供弱键,JJWT 将拒绝它并抛出异常。JWT 规范以及 JJWT 规定密钥长度的原因在于,如果不遵守算法的强制密钥属性,特定算法的安全模型可能完全崩溃,实际上根本没有安全性,这将会导致完全不安全的 JWT。

算法使用要求

HMAC-SHA

JWT HMAC-SHA 签名算法 HS256,HS384 和 HS512 需要一个密钥,该密钥至少与 RFC 7512第3.2节 中算法的签名(摘要)长度一样多。 这意味着:

  1. HS256是HMAC-SHA-256,它产生256位(32字节)长的摘要,因此HS256要求您使用至少32字节长的密钥。
  2. HS384是HMAC-SHA-384,它产生384位(48字节)长的摘要,因此HS384要求您使用至少48字节长的密钥。
  3. HS512是HMAC-SHA-512,它产生512位(64字节)长的摘要,因此HS512要求您使用至少64字节长的密钥。

RSA

JWT RSA 签名算法 RS256,RS384,RS512,PS256,PS384 和 PS512 都要求每个RFC 7512第3.3和3.5节的最小密钥长度(也称为RSA模数位长度)为2048位。 任何小于此值的内容(例如1024位)都将被拒绝,并抛出异常 InvalidKeyException。

也就是说,为了与最佳实践保持一致并增加键长度,JJWT建议考虑使用的:

  1. RS256和PS256至少有2048位密钥
  2. RS384和PS384至少3072位密钥
  3. RS512和PS512至少4096位密钥

Elliptic Curve

JWT 椭圆曲线签名算法 ES256,ES384 和 ES512 都需要最小密钥长度(也称为椭圆曲线顺序位长度),其至少与RFC 7512第3.4节中算法签名的各个R和S分量一样多。 这意味着:

  1. ES256要求您使用至少256位(32字节)长的私钥。
  2. ES384要求您使用长度至少为384位(48字节)的私钥。
  3. ES512要求您使用长度至少为512位(64字节)的私钥。

创建 JWS

首先,可以按如下方式创建 JWS:

  • 使用 Jwts.builder() 方法创建 JwtBuilder 实例。
  • 调用 JwtBuilder 方法根据需要添加标头参数和声明。
  • 指定要用于对 JWT 进行签名的 SecretKey 或非对称 PrivateKey。
  • 最后,调用 compact() 方法进行压缩和签名,生成最终的 jws。
String jws = Jwts.builder()     // (1)
.setSubject("JDKONG") // (2)
.signWith(key) // (3)
.compact(); // (4)

设置 Header Parameters

JWT Header提供关于JWT Claims相关的内容,格式和加密操作的元数据。

如果需要设置一个或多个JWT头参数,则可以根据需要简单地多次调用 JwtBuilder#setHeaderParam,如下所示:

String jws = Jwts.builder()
.setHeaderParam("kid", "myKeyId")
// ... etc ...

每次调用 setHeaderParam 时,它只是将键值对附加到内部 Header 实例,如果键值已经存在,则会覆盖任何现有的同名键/值对。

注意:不需要设置 alg 或 zip 标头参数,因为 JJWT 会根据使用的签名算法或压缩算法自动设置它们。

除此之外,你还可以使用另外两种方式,设置JWT Header,如下所示:

方式二:

Header header = Jwts.header();
populate(header); //implement me
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...

方式三:

Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...

方式2 与 方式3 需要注意的是:调用 setHeader 将覆盖任何现有的同名的 key/value 对。 在所有情况下,JJWT 仍将设置(并覆盖)任何 alg 和 zip 标头,无论它们是否在指定的标头对象中。

设置 Claims

Claims 是 JWT 的正文部分,包含JWT创建者希望向JWT收件人提供的信息。

标准的 Claims

String jws = Jwts.builder()
.setIssuer("me")
.setSubject("Bob")
.setAudience("you")
.setExpiration(expiration) //a java.util.Date
.setNotBefore(notBefore) //a java.util.Date
.setIssuedAt(new Date()) // for example, now
.setId(UUID.randomUUID()) //just an example id

/// ... etc ...

自定义 Claims

如果需要设置一个或多个与上面显示的标准 setter 方法声明不匹配的自定义声明,可以根据需要多次调用 JwtBuilder#claim 声明:

String jws = Jwts.builder()
.claim("hello", "world")
// ... etc ...

每次调用 claim 时,它只是将键值对附加到内部 claims 实例,如果键值已经存在,则会覆盖任何现有的同名key/value对。

同上,你还可以使用另外两种方式,设置JWT Claims,如下所示:

方式 2:

Claims claims = Jwts.claims();

populate(claims); //implement me
String jws = Jwts.builder()
.setClaims(claims)
// ... etc ...

方式 3:

Map<String,Object> claims = getMyClaimsMap(); //implement me
String jws = Jwts.builder()
.setClaims(claims)
// ... etc ...

同样,方式2 与 方式3 需要注意的是:调用 setClaims 将覆盖任何现有的同名的key/value对。

签名 Signing Key

建议通过调用 JwtBuilder 的 signWith 方法来指定签名密钥,并让 JJWT确定指定密钥允许的最安全算法:

String jws = Jwts.builder()
// ... etc ...
.signWith(key) // <---
.compact();

例如,如果使用长度为256位(32字节)的 SecretKey 调用 signWith,则对于 HS384 或 HS512,它不够强大,因此 JJWT将使用 HS256自动对 JWT进行签名。

使用 signWith 时,JJWT 还会自动使用相关的算法标识符设置所需的 alg 头。

类似地,如果使用长度为 4096位的 RSA PrivateKey调用 signWith,JJWT 将使用 RS512算法并自动将 alg头设置为 RS512。

注意:你不能用 PublicKeys 签署 JWT,因为这总是不安全的。 JJWT将拒绝任何指定的 PublicKey的方式签名,并抛出异常:InvalidKeyException。

自定义 签名算法

在某些特定情况下,您可能希望覆盖给定键的 JJWT默认选定算法。

例如,如果有一个 2048位的 RSA PrivateKey,JJWT会自动选择 RS256算法。 如果使用 RS384或 RS512,可以使用重载的 signWith方法手动指定它,该方法接受 SignatureAlgorithm作为附加参数:

   .signWith(privateKey, SignatureAlgorithm.RS512) // <---
.compact();

解析 JWS

按如下方式解析JWS:

  • 使用 Jwts.parser() 方法创建 JwtParser 实例。
  • 指定要用于验证 JWS 签名的 SecretKey 或非对称 PublicKey
  • 最后,使用 jws String 调用 parseClaimsJws(String) 方法,生成原始 JWS。
  • 整个调用将包装在 try/catch 块中,以防解析或签名验证失败。
Jws<Claims> jws;

try {
jws = Jwts.parser() // (1)
.setSigningKey(key) // (2)
.parseClaimsJws(jwsString); // (3)
// we can safely trust the JWT
catch (JwtException ex) { // (4)
// we cannot use the JWT as intended by its creator
}

校验 Key

阅读 JWS时,最重要的事情是指定用于验证 JWS加密签名的密钥。 如果签名验证失败,则无法安全地信任此 JWT,应将其丢弃。

那么我们使用哪个密钥进行验证?

如果 jws是使用 SecretKey签名的,则应在 JwtParser上指定相同的 SecretKey。 例如:

Jwts.parser()
.setSigningKey(secretKey) // <----
.parseClaimsJws(jwsString);

如果 jws是使用 PrivateKey签名的,那么应该在 JwtParser上指定该密钥相应的 PublicKey(不是PrivateKey)。 例如:

Jwts.parser()
.setSigningKey(publicKey) // <---- publicKey, not privateKey
.parseClaimsJws(jwsString);

编写一个 JWT 工具类

public class JwtUtil {

// 注意,这里使用 secretKeyFor 方法自动随机生成一个适合指定编码长度的密钥,避免硬编码出错,以及安全问题
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 不过正式的开发环境,这个密钥最好不要这样搞,第一次生成之后记录下来就行了,不然每次重启服务一次,全部 JWT 都失效了

public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}

private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}

private String createToken(Map<String, Object> claims, String subject) {

return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
// 这里需要显示指定使用 HS256(注意,上面只是生成一个适合长度的密钥,本体它还是一个普通字串)
.signWith(SECRET_KEY, SignatureAlgorithm.HS256).compact();
}

public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

JJWT 生成 JWT 案例

@Test
void contextLoads() throws IOException {
// 获取系统时间
long now = System.currentTimeMillis();
// 设置过期时间
long exp = now + 60 * 1000 * 2; // 两分钟
// 自定义 Claims
Map<String,Object> map = new HashMap<>();
map.put("name", "alsritter");
map.put("id", "1234");

JwtBuilder jwtBuilder = Jwts.builder()
// 声明的标识 {"jti":"888"}
.setId("888")
// 主体,用户 {"sub" : "Rose"}
.setSubject("Rose")
// 创建日期 {"ita" : "12345"}
.setIssuedAt(new Date())
// 签名,参数1:算法,参数2:盐
.signWith(SignatureAlgorithm.HS256, "this_is_secret_key")
// 添加 Claims 有两种方式,一种是一个个2加,一种是直接使用 map
.claim("age",18)
.claim("right","admin")
.addClaims(map)
// 设置过期时间(过期了就会像上面那个 JWT 库一样抛异常)
.setExpiration(new Date(exp));

// 获取 jwt 的 token
String token = jwtBuilder.compact();

System.out.println(token);
// 三部分的 Base64 解码
System.out.println("----------------------");
String[] split = token.split("\\.");
// 可以使用 JJWT 自带的解密方法 Base64Codec.BASE64.decodeToString(split[0]);
// 这里用 JDK 自带的
System.out.println(new String(new BASE64Decoder().decodeBuffer(split[0])));
System.out.println(new String(new BASE64Decoder().decodeBuffer(split[1])));
// 最后一部分无法解密
System.out.println(split[2]);
}

JJWT 解析 JWT 案例

@Test
void contextLoads() throws IOException {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjA1NzY1MDIxfQ.-s_8OTfi3w5QSJTQcOAz-HVVAJ7PquLshB1bfA03_3k";
// 解析 token 获取负载中的声明对象
Claims claims = Jwts.parser()
.setSigningKey("this_is_secret_key")
.parseClaimsJws(token)
.getBody();

// 打印声明的属性
System.out.println("id:" + claims.getId());
System.out.println("subject:" + claims.getSubject());
System.out.println("issuedAt:" + claims.getIssuedAt());
// 取得自定义的
System.out.println("right:" + claims.get("right"));
}