# 操作 JWT:nimbus-jose-jwt 库
nimbus-jose-jwt、jose4j、java-jwt 和 jjwt 是几个 Java 中常见的操作 JWT 的库。就使用细节而言,nimbus-jos-jwt(和jose4j)要好于 java-jwt 和 jjwt 。
nimbus-jose-jwt 官网 (opens new window)
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
# 1. 相关概念
# JWT 和 JWS
这里我们需要了解下 JWT、JWS、JWE 三者之间的关系:
JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息。
JWS(JSON Web Signature)和 JWE(JSON Web Encryption)是 JWT 规范的两种不同实现,我们平时最常使用的实现就是 JWS 。
简单来说,JWT 和 JWS、JWE 类似于接口与实现类。由于,我们使用的是 JWS ,所以,后续内容中,就直接列举 JWS 相关类,不再细分 JWS 和 JWE 了,numbus-jose-jwt 中的 JWE 相关类和接口我们也不会使用到。
# 加密算法
另外,还有一对可能会涉及的概念:对称加密和非对称加密:
『对称加密』指的是使用相同的秘钥来进行加密和解密,如果你的秘钥不想暴露给解密方,考虑使用非对称加密。在加密方和解密方是同一个人(或利益关系紧密)的情况下可以使用它。
『非对称加密』指的是使用公钥和私钥来进行加密解密操作。对于加密操作,公钥负责加密,私钥负责解密,对于签名操作,私钥负责签名,公钥负责验证。非对称加密在 JWT 中的使用显然属于签名操作。在加密方和解密方是不同人(或不同利益方)的情况下可以使用它。
nimbus-jose-jwt 支持的算法都在它的 JWSAlgorithm 和 JWEAlgorithm 类中有定义。
例如:JWSAlgorithm algorithm = JWSAlgorithm.HS256
# 2. 核心 API 介绍
# 加密过程
在 nimbus-jose-jwt 中,使用 Header 类代表 JWT 的头部,不过,Header 类是一个抽象类,我们使用的是它的子类 JWSHeader 。
创建头部对象:
JWSHeader jwsHeader = new JWSHeader.Builder(algorithm) // 加密算法 .type(JOSEObjectType.JWT) // 静态常量 .build();
另外,你可以通过
.getParsedBase64URL()
方法求得头部信息的 Base64 形式(这也是 JWT 中的实际头部信息):header.getParsedBase64URL();
使用 Payload 类的代表 JWT 的荷载部分,
创建荷载部对象
Payload payload = new Payload("hello world");
另外,你可以通过
.toBase64URL()
方法求得荷载部信息的 Base64 形式(这也是 JWT 中的实际荷载部信息):payload.toBase64URL();
签名部分没有专门的类表示,只有通用类 Base64URL ,而且签名部分并非你自己创建出来的,而是靠
头部 + 荷载部 + 加密算法
算出来的。在 nimbus-jose-jwt 中,签名算法由 JWSAlgorithm 表示。
注意
在创建 JWSHeader 对象时就需要指定签名算法,因为在标准中,头部需要保存签名算法名字。
用头部和荷载部分,再加上指定的签名算法和密钥来生成签名部分的过程,在 nimbus-jose-jwt 中被称为『签名(sign)』。
nimbus-jose-jwt 专门提供了一个签名器 JWSSigner ,用来参与到签名过程中。密钥就是在创建签名器的时候指定的:
JWSSigner jwsSigner = new MACSigner(secret);
最终,整个 JWT 由一个 JWSObject 对象表示:
JWSObject jwsObject = new JWSObject(jwsHeader, payload); // 进行签名(根据前两部分生成第三部分) jwsObject.sign(jwsSigner);
在 nimbus-jose-jwt 中 JWSObject 是有状态的:未签名、已签名和签名中。很显然,在执行外
.sign()
方法之后,JWSObject 对象就变成了已签名状态。当然,我们最终『要』的是 JWT 字符串,而不是对象,这里接着对代表 JWT 的 JWSObject 对象调用
.serialize()
方法即可:String token = jwsObject.serialize();
# 解密
反向的解密和验证过程核心 API 就 2 个:JWSObject 的静态方法 parse 方法和验证其 JWSVerifier 对象。
JWSObject.parse()
方法是上面的 serialize 方法的反向操作,它可以通过一个 JWT 串生成 JWSObject 。有了 JWObject 之后,你就可以获得 header 和 payload 部分了。
如果你想直接验证 JWSObject 对象的合法性,你需要创建一个 JWSVerifier 对象。
JWSVerifier jwsVerifier = new MACVerifier(secret);
然后直接调用 jwsObject 对象的 verify 方法:
if (!jwsObject.verify(jwsVerifier)) {
throw new RuntimeException("token 签名不合法!");
}
# 3. 官网的 HS256 示例
import java.security.SecureRandom;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
// Generate random 256-bit (32-byte) shared secret
SecureRandom random = new SecureRandom();
byte[] sharedSecret = new byte[32];
random.nextBytes(sharedSecret);
// Create HMAC signer
JWSSigner signer = new MACSigner(sharedSecret);
// Prepare JWS object with "Hello, world!" payload
JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.HS256), new Payload("Hello, world!"));
// Apply the HMAC
jwsObject.sign(signer);
// To serialize to compact form, produces something like
// eyJhbGciOiJIUzI1NiJ9.SGVsbG8sIHdvcmxkIQ.onO9Ihudz3WkiauDO2Uhyuz0Y18UASXlSc1eS0NkWyA
String s = jwsObject.serialize();
// To parse the JWS and verify it, e.g. on client-side
jwsObject = JWSObject.parse(s);
JWSVerifier verifier = new MACVerifier(sharedSecret);
assertTrue(jwsObject.verify(verifier));
assertEquals("Hello, world!", jwsObject.getPayload().toString());
# 4. 在 Payload 中存对象
在上例(和官方示例中)payload 中存放的是简单的字符串,其实,更方便更有使用价值的是存入一个 json 串。这里有 2 种方案:
使用 net.minidev.json.JSONObject
JSONObject obj = new JSONObject(); obj.put(usernameKey, username); obj.put("sub", ...); obj.put("iat", ...); obj.put("exp", ...); obj.put("jti", ...); obj.put("username", ...); Payload payload = new Payload(obj);
自定义 JavaBean ,再转成 JSON 串:
public class Claims { private String sub; // "主题" private Long iat; // "签发时间" private Long exp; // 过期时间 private String jti; // JWT的ID private String username; // "用户名称" // getter / setter }
这样在创建 Payload 时,需要多一步转换操作:
ObjectMapper mapper = new ObjectMapper(); // 这里使用的是 Jackson 库 // 将负载信息封装到Payload中 Payload payload = new Payload(mapper.writeValueAsString(claims));
反向的取出内容时,也是一样的道理。