JJWT,是一款适用于 Java 和 Android 的 JSON Web Token(JWT)库。
JJWT, 也称为Java JWT ,全称是 Java JSON Web Token。
JJWT完全基于 JWT、JWS、JWE、JWK 和 JWA RFC 规范以及 Apache 2.0 许可条款下的开源。该库由 Okta 的高级架构师 Les Hazlewood 创建,由一个贡献者社区支持和维护。
JJWT 维护在GitHub上 ,地址是 https://github.com/jwtk/jjwt,其没有专门的网站,开发文档也不是很丰富。
JJWT的优点
- 功能齐全
- 安全
- 流式接口开发,方便,可读性强。在IDE中有自动补全代码的功能
- 完全符合RFC规范
- 100%的代码测试覆盖率
- 使用所有标准JWS算法创建、解析和验证数字签名的紧凑JWT
JJWT 签名算法
标示符 | 签名算法 |
---|---|
HS256 | 使用SHA-256的HMAC |
HS384 | 使用SHA-384的HMAC |
HS512 | 使用SHA-512的HMAC |
ES256 | 使用P-256 和SHA-256的ECDSA |
ES384 | 使用P-384 和SHA-384的ECDSA |
ES512 | 使用P-512 和SHA-512的ECDSA |
RS256 | 使用SHA-256的RSASSA-PKCS-v1_5 |
RS384 | 使用SHA-384的RSASSA-PKCS-v1_5 |
RS512 | 使用SHA-512的RSASSA-PKCS-v1_5 |
PS256 | SHA-256 和MGF1 with SHA-256 的RSASSA-PSS |
PS384 | RSASSA-PSS using SHA-384 and MGF1 with SHA-384 |
PS512 | RSASSA-PSS using SHA-512 and MGF1 with SHA-512 |
EdDSA | Edwards-curve Digital Signature Algorithm |
- HMAC(Hash-based Message Authentication Code)是一种基于哈希函数的消息认证码算法。它使用一个密钥以及输入的消息作为输入,通过执行特定的哈希函数运算,生成一个长度固定的认证码。HMAC可以用于验证消息的完整性和验证消息的来源身份。
- 使用所有标准JWE加密算法创建、解析和解密加密的紧凑JWT
- 用于获取JWE加密和解密密钥的所有密钥管理算法
- 使用本地Java密钥类型以所有标准JWA密钥格式创建、解析和验证JSON Web密钥(JWK)
JJWT依赖导入
在项目的pom.xml 添加如下配置:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>JJWT_RELEASE_VERSION</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>JJWT_RELEASE_VERSION</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>JJWT_RELEASE_VERSION</version>
<scope>runtime</scope>
</dependency>
- JJWT_RELEASE_VERSION 对应JJWT的发布版本,目前最新版本是0.11.5,关于JJWT的发布版本, 可以参考:https://github.com/jwtk/jjwt/tags
- 因为JWT是以JSON格式进行数据的传递, 这里使用的JOSN包是jackson,如果要使用Gson的JSON包,可以导入 jjwt-gson
如果有以下使用场景,需要额外导入 bcprov-jdk15on 的库
- JDK 10或更早的版本, 需要使用RSASSA-PSS (PS256, PS384, PS512) 签名算法
- JDK 10或更早的版本, 需要使用EdECDH(X25519 或 X448)椭圆曲线 Diffie-Hellman 加密。
- JDK14或更早版本, 需要使用EdDSA (Ed25519 or Ed448) 椭圆曲线签名算法
注意:如果是JDK15及之后的版本, 则不需要添加
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
<scope>runtime</scope>
</dependency>
为什么jjwt-impl 的scope是runtime
JJWT 保证除 jjwt-impl .jar 之外的所有工件的语义版本控制兼容性。 对于 jjwt-impl .jar 没有这样的保证,并且该 .jar 中的内部更改可能随时发生。 切勿将 jjwt-impl .jar 添加到具有编译范围的项目中 - 始终使用运行时范围声明它。
JJWT示例
有签名的JWT令牌:
@Test
public void genJws() {
//1. 产生令牌
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //HMAC-SHA-256
String jws = Jwts.builder().setSubject("Osxm").signWith(key).compact();
System.out.println("jws="+jws);
//2. 解析令牌
String str = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jws).getBody().getSubject();
System.out.println(str);
}
- 签名的JWT, 如果在解析的时候没有设置签名key会提示如下错误:
java.lang.IllegalArgumentException: A signing key must be specified if the specified JWT is digitally signed.
- 所以 ,可以通过异常捕获判断是否是有效的签名token 。
设置 JWT 头部
JWT 标头是一个 JSON 对象,提供有关内容、格式以及与 JWT 有效负载相关的任何加密操作的元数据。 JJWT 提供了多种设置整个标头和/或多个单独标头参数(名称/值对)的方法。
可以使用setHeaderParam() 添加头部的参数。
@Test
public void setHeader() {
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jws = Jwts.builder()
.setHeaderParam("head1", "head1 value")
.setHeaderParam("head2", "head2 value")
.setSubject("Osxm").signWith(key).compact();
System.out.println("jws="+jws);
}
负载
设置普通的文本
String jwt = Jwts.builder().setPayload("普通文本的负载").compact();
JWT 有效负载可能包含 JWT 接收者的断言或声明,而不是内容字节数组。 在这种情况下,有效负载是声明 JSON 对象,并且 JJWT 支持使用类型安全构建器方法创建声明。
@Test
public void payload() {
Date now = new Date();
String jwt = Jwts.builder().setPayload("普通文本的负载").compact();
Jwts.builder()
.setIssuer("Osxm")
.setSubject("Demo JWT")
.setAudience("ITs")
.setExpiration(new Date(now.getTime() + 3600 * 1000)) //过期时间, 1小时
.setNotBefore(now) //最早生效时间
.setIssuedAt(now)
.setId(UUID.randomUUID().toString())
.compact();
}
- JSON Web Tokens(JWT)中,“notBefore”(nbf)是一个标准声明,代表了令牌在此之前不被视为有效。它指示令牌可以开始使用或接受的最早时间(以秒或特定日期和时间表示)。
增加其他的声明:
.claim("myname", "myvalue")
签名密钥
签名密钥
用公钥签署 JWT不安全。 JJWT 将拒绝任何指定的 PublicKey 进行签名,并抛出 InvalidKeyException。
####SecretKey 的格式
使用 HMAC-SHA 算法对 JWS 进行签名,自行设定密钥字符串或编码字节数组,则需要将其转换为 SecretKey 实例以用作 signWith 方法参数。
JWE - 完全加密的JWT
上面的示例中 JWT本身没有保护,签名有加密保护的场景(JWS)。头部和负载的数据可以看, 数字签名可以保证没有被人篡改。
但是某些场景下,负载中会包含一些敏感信息。这样就需要一个完全加密的JWT ,称为 “JWE”。WE 使用加密技术来确保有效负载保持完全加密和身份验证,因此未经授权的各方无法看到其中的数据,也无法在不被发现的情况下更改数据。 具体来说,JWE 规范要求使用关联数据算法的身份验证加密来完全加密和保护数据。
根据密钥的不同格式,
- 编码的字节数组:
SecretKey key = Keys.hmacShaKeyFor(encodedKeyBytes);
- Base 64编码字符串:
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
- Base64URL 编码的字符串:
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
- 原始(未编码)字符串(例如密码字符串):
SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
注意: secretString.getBytes() (不提供字符集)的方式是不推荐的。此外使用原始密码字符串,不可避免地会导致钥匙脆弱或敏感, 尽量使用安全随机密钥。
@Test
public void key() {
//1. 方式1 ,使用密码字符串的字节数组
String mykeyString = "thisismykeythisismykeythisismykeythisismykey";
Key secretKey = Keys.hmacShaKeyFor(mykeyString.getBytes(StandardCharsets.UTF_8));
//2. 使用Java本身获取
Key secretKey2 = new SecretKeySpec(mykeyString.getBytes(StandardCharsets.UTF_8),"HmacSHA256");
//3. JJWT的SignatureAlgorithm
Key secretKey3 = new SecretKeySpec(mykeyString.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());
//4. 随机字符串
Key secretKey4 = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String keyString = Encoders.BASE64.encode(secretKey4.getEncoded());
System.out.println("keyString="+keyString);
}
JWT压缩
如果 JWT 负载很大(包含大量数据),需要压缩 JWT 以减小其大小。 请注意,这不是所有 JWT(仅限 JWE)的标准功能,并且其他 JWT 库不太可能支持非 JWE 令牌。 然而,JJWT 支持 JWS 和 JWE 的压缩。
非 JWS 标准:JJWT 支持 JWS 压缩,但它不是 JWS 的标准功能。 JWT RFC 规范仅针对 JWE 对此进行了标准化,并且 JWS 的其他 JWT 库不太可能支持它。 仅当确定 JJWT(或支持 JWS 压缩的其他库)将解析 JWS 时才使用 JWS 压缩
读取 JWT
Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(mykeyString.getBytes(StandardCharsets.UTF_8)).build().parseClaimsJws(jwsString);
String issuer = jws.getBody().getIssuer();
System.out.println("issuer="+issuer);
// 创建JWT解析器,并设置签名密钥解析器
JwtParser jwtParser = Jwts.parserBuilder()
.setSigningKeyResolver(new SigningKeyResolverAdapter() {
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
// 在这里实现自定义的签名密钥解析逻辑,并返回解析的密钥
// ...
}
})
.build();
// 使用解析器解析JWT
Jws<Claims> jws = jwtParser.parseClaimsJws(jwt);
Claims claims = jws.getBody();