本文参考的是jjwt官方github,链接在此,本文会持续跟进jjwt的最新版本
GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android
简介
JJWT(Java JWT)是Java平台上相当流行的用于生成Json Web Token的库,其更新速度非常快,导致网上许多教程在如今看来都已经过时。本文将对最新的JJWT官方github页进行中文解释,力求做到与官方同步。
最简单的JWT结构由以下两个部分组成:
payload
有效负载,是JWT中的主要数据,可以是你想要的任何数据。header
标头。 具有 名称/值 对的JSON对象,表示有关有效负载和消息本身的元数据
上述两个部分由 . 隔开,所以可以给出一个JWT例子:eyJhbGciOiJub25lIn0.VGhlIHRydWUgc2lnbiBvZiBpbnRlbGxpZ2VuY2UgaXMgbm90IGtub3dsZWRnZSBidXQgaW1hZ2luYXRpb24u.
其中:
eyJhbGciOiJub25lIn0
是标头(header)VGhlIHRydWUgc2lnbiBvZiBpbnRlbGxpZ2VuY2UgaXMgbm90IGtub3dsZWRnZSBidXQgaW1hZ2luYXRpb24u
是有效负载(payload)
对于JWS来说,会新增一个**签名(signature)**部分。对于JWE来说,则是五个部分JOSE Header、JWE Encrypied Key、Initialization Vector、Ciphertext和Authentication Tag。
名词解释
JWT | Java Web Token 一种通用的基于文本的信息传递格式,可用于传递任意类型的数据 |
---|---|
JWS | JSON Web Signature 对JSON数据结构进行签名的标准,用于确保数据完整和来源的真实性 |
JWE | JSON Web Encryption 对JSON数据结构进行加密的标准,用于确保数据的机密性 |
JWS和JWE的区别在于,
- JWS中的数据是可以被任何人查看的,其只能够保证信息在传递过程中未被篡改;
- JWE则保护了数据不被未授权方查看,也能够保证信息未被篡改,因此数据涉及到隐私时应当使用JWE。
声明(Claim)
在许多场景下,开发者习惯使用JSON的有效负载(payload)来表示用户或主机或其他的身份概念的数据。这种情况下的负载(payload)就称为声明(Claim)。大家常用的比如说用户名/ip地址等都可以归属到声明之中。
自定义声明(Custom Claims)
毫无疑问,jjwt也是支持自定义声明的,使用Claim()
方法即可在payload中插入自己想要的键值对
快速开始
安装
Maven
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<!-- 如果你想要使用接下来的这些功能,请取消注释下面依赖项的注释:
- JDK 10 或更早, 并且你想要使用RSASSA-PSS (PS256, PS384, PS512)签名算法.
- JDK 10 或更早, 并且你想要使用EdECDH (X25519 or X448) Elliptic Curve Diffie-Hellman加密.
- JDK 14 或更早, 并且你想要使用EdDSA (Ed25519 or Ed448) Elliptic Curve签名算法.
对于JDK15或以上的版本来说,这些算法是不必要的.
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId> or bcprov-jdk15to18 on JDK 7
<version>1.76</version>
<scope>runtime</scope>
</dependency>
-->
使用JJWT进行JWS生成
// key生成部分
SecretKey key = Jwts.SIG.HS256.key().build();
//jws生成部分
String jws = Jwts.builder()
.subject("Joe")
.claim("name","Mike")
.claim("address","Beijing")
.signWith(key)
.compact();
key生成部分
首先SecretKey key = Jwts.SIG.HS256.key().build();
进行调试可以看到,生成的是一个SecertKey对象,包含两个属性:key
和algorithm
,key
是随机生成的一个HMAC密钥,algorithm
则说明当前使用的签名算法。需要保管好这个key,后续的jws解析也需要用到它.
jws生成部分
然后是jws生成部分上文生成的的jws是一条JWS,.claim("name","Mike")
指payload中新增一条"name":“Mike"的键值对.claim("address","Beijing")
用于指出claim()
方法是可以多次重复使用的.subject("Joe")
指payload中的键sub的值为"Joe”.signWith(key)
指使用生成的key进行签名.compact()
指将其压缩为String格式
使用JJWT进行JWS解析
// jws解析
Jws<Claims> claims= Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(jws);
System.out.println(claims.getPayload().get("sub"));
System.out.println(claims.getPayload().get("name"));
System.out.println(claims.getPayload().get("address"));
.verifyWith(key)
指的是选用解析jws用的key,上面用过,此处调用即可.paraseSignedClaims(jws)
指选用jws这个字符串进行解析
得到一个Jws类型的对象claimsclaims.getPayload().get("sub")
指从payload中获取键"sub"对应的值,另外两个亦然
创建JWT
根据以下步骤创建JWT:
- 使用
Jwts.builder()
方法来创建一个JwtBuilder
实例 - (可选)根据需求设置header参数
- 调用构建器方法来设置有效负载 content **或 **claims
- (可选)如果需要对JWT进行数字签名或者加密时,调用
signWith
或encryptWith
方法(分别对应JWS和JWE) - 调用
compact()
方法来生成紧凑的JWT字符串
例如
String jwt = Jwts.builder() // (1)
.header() // (2) 可选
.keyId("aKeyId")
.and()
.subject("Bob") // (3) JSON Claims, 或
//.content(aByteArray, "text/plain") // 任意字节内容
.signWith(signingKey) // (4) 数字签名, 或
//.encryptWith(key, keyAlg, encryptionAlg) // 加密
.compact(); // (5)
- JWT payload可以是字符数组(使用
content
方法),也可以是JSON Claims(使用subject
,claims
等方法)但是不能同时既是字符数组又是JSON Claims - 数字签名(
signWith()
方法)和加密(encryptWith()
方法)同上,不能同时使用
JWT Header
JWT Header 是一个JSON对象,提供有关 内容,格式以及与JWT payload相关的任何加密操作的元数
据。JJWT提供了多种设置整个Header和/或多个单独Header参数(键值对)
JwtBuilder Header
设置你想要的JWT Header参数,最简单的方式就是使用JwtBuilder的header()
构建器,然后再调用and()
方法返回到JwtBuilder中,配置payload
或者是signature
部分进行进一步配置,例如
String jwt = Jwts.builder()
.header() // <----
.keyId("aKeyId")
.x509Url(aUri)
.add("someName", anyValue) //<--- 支持自定义Header
.add(mapValues) //<--- 支持输入参数Map
// ... etc ...
.and() // 回到JwtBuilder。.and()可以看作是header部分的结束
.subject("Joe") // 继续调用JwtBuilder,可以接着写payload部分了
// ... etc ...
.compact();
JwtBuilder()
的header()
构建器也支持自动计算X.509指纹 等其他功能。
小贴士:
- 自动Header功能:你无需手动设置header中的
alg
,enc
或zip
,JJWT会自动设置(快说谢谢JJWT orz)
Jwts HeaderBuilder
原文中建议的是使用Jwts.Builder进行构建,因此此处我** 暂时 **不翻译,先行翻译比较重要的构建payload部分。
JWT Payload
JWT的payload
可以是任何内容——任何可以表示为 字节数组 的内容,例如文本、图像、文档等等。但由于JWT header总是JSON格式,因此payload也可以是JSON,特别是用于表示身份声明(也就是Claim)。
因此,JwtBuilder支持两种不同的payload选项:
- content:如果您希望payload是任意 字节数组 内容
- Claims(以及支持的其他辅助方法):如果您希望payload是一个JSON Claims Object。
这两种选项可以使用任意一种,但 不能同时使用!!!!!。同时使用会导致compact()抛出异常。
任意内容
你可以使用JwtBuilder中的content()方法将payload设置为任意内容,需要注意的是,只有最后一次调用的content()
方法会生效。例如
byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8);
String jwt = Jwts.builder()
.content(content, "text/plain") //<--- 任意内容,第二个参数会设置为header中的cty
.content(content) //<--- 同样是任意内容,第二个参数可以不写
.content(content) //<--- 只有这个content会生效
// ... etc ...
.build();
给个案例吧,官方有点啰嗦
String jws = Jwts.builder()
.content("你好呀,祝你开心哈哈","你好你好")
.content("早上好中午好晚上好")
.content("哈哈哈","cty是哈哈哈")
.signWith(key)
.compact();
解析出来的jwt结果是
Header:
{
"cty": "cty是哈哈哈",
"alg": "HS256"
}
Payload:
哈哈哈
很明显,最后只有.content("哈哈哈","cty是哈哈哈")
这一行生效了。
第二个参数的标准写法参照RFC 7515: JSON Web Signature (JWS)。官方 强烈推荐使用 双参数方法,因为可以让接收者便利的知晓 payload 中数据的格式。
JWT Claims
JWT payload可能会包含JWT接收者的断言或者是身份认证信息,而不是字符数组内容。在这种情况下,payload就是一个 Claims JSON Object。
Standard Claims(标准声明)
在RFC7519标准中,存在着数个标准声明(standard claim)。这些Claims并非强制性要求。JJWT为每一个标准声明都提供了方便的构造方法。例如subject()
、iss()
等方法。标准声明表:
issuer
: sets the iss(Issuer) Claimsubject
: sets the sub(Subject) Claimaudience
: sets the aud(Audience) Claimexpiration
: sets the exp(Expiration Time) ClaimnotBefore
: sets the nbf(Not Before) ClaimissuedAt
: sets the iat(Issued At) Claimid
: sets the jti(JWT ID) Claim
举个栗子:
String jws = Jwts.builder()
.issuer("me")
.subject("Bob")
.audience().add("you").and()
.expiration(expiration) //java.util.Date类
.notBefore(notBefore) //java.util.Date类
.issuedAt(new Date()) // 当前时间
.id(UUID.randomUUID().toString()) //示例id
/// ... etc ...
自定义Claims
如果你需要设置和上面不一样的自定义声明,你可以使用JwtBuilder的claim()
方法,可以使用多次,每使用一次都会向payload中新增一个键-值对。
String jws = Jwts.builder()
.claim("hello", "world")
.claim("你好", "我好")
// ... etc ...
当然,你也可以直接往payload里面加入Map
Map<String,?> claims = getMyClaimsMap(); //implement me
String jws = Jwts.builder()
.claims(claims)
// ... etc ...
JWT 压缩
一般情况下的jwt palyload都用不到压缩,此处略过。
JWT解析
读取JWT的步骤如下:
- 使用
Jwts.parser()
方法来创建一个JwtParserBuilder
实例 - (可选)如果想要对JWT进行加密或者是签名,可以调用
keyLocator
,verifyWith
或decryptWith
方法 - 调用
JwtParser
的build()
方法创建并返回线程安全的JwtParser
- 根据你所需要的 JWT 类型,调用各种
parse
方法。 - 将整个
parse
调用封装在一个try-catch
代码块中以保证安全
举个梨子:
待会再写
提示:如果你能够100%肯定自己的解析器只会接收特定类型的JWT,那就可以不用这里的通用解析方式,而可以调用parseSignedClaims
、parseEncryptedClaims
等类型安全的方法。
这些解析方法将会返回你所期望的类型安全的JWT,比如说Jws<Claims>
、Jwe<byte[]>
而不是通用的Jwt<?,?
实例
常量解析密钥
如果JWT需要解析一个JWS或者是JWE,那肯定要一个密钥(key)来解析或者签名,此处这里讨论常量密钥的情况
- 如果解析的是JWS,且该JWS是使用
SecretKey
签名的,那么应当在JwtParseBuilder指定相同的SecretKey
。栗子:Jwts.parser() .verifyWith(secretKey) // <---- .build() .parseSignedClaims(jwsString);
- 如果解析JWS且该JWS由私钥签名,那么应该在JwtParseBuilder上使用公钥,和上面一样。
Jwts.parser() .verifyWith(publicKey) // <---- 这里是公钥哦,不是私钥(废话) .build() .parseSignedClaims(jwsString);
- 如果解析的是JWE且直接由
SecretKey
加密,那么同理使用SecretKey
Jwts.parser() .decryptWith(secretKey) // <---- or a Password from Keys.password(charArray) .build() .parseEncryptedClaims(jweString);
- 如果解析JWE使用与PublicKey一起使用的密钥算法加密,则应该在JwtParserBuilder上指定该密钥对应的私钥(而不是公钥)
Jwts.parser() .decryptWith(privateKey) // <---- privateKey, not publicKey .build() .parseEncryptedClaims(jweString);
未完待续....
2024/07/16日记:本来没啥状态,就鬼使神差打开csdn写这玩意了。。。。
2024/10/24日记:突然记起来这事,继续写点