JJWT
目录
JWT 的声明一般被用来在客户端和服务端间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑信息
¶概述
JJWT 旨在成为最易使用和理解的库,用于在 JVM 和 Android 上创建和验证 JSON Web 令牌,JWT本身是支持加密签名的,在使用签名的JWT时,需要注意一下两点:
真实性和完整性 保证JWT包含可以信任的信息。 如果JWT未通过真实性或完整性检查,应该始终拒绝JWT,因为我们无法信任它
¶Hello World
仍然按照常见的套路(依赖,HelloWorld,详细概念)三步骤学习一个第三方库
¶依赖
1 | <dependency> |
¶创建 JWT
1 | public static void main(String[] args) { |
上述代码中,构建了一个主题为pineapple-man的 JWT,使用的是HMAC-SHA-256算法的密钥对 JWT 进行签名,并在最终将它压缩成 String 形式
¶解析 JWT
1 | public static void main(String[] args) { |
将之前的密钥用于验证JWT的签名,如果它无法验证JWT,则抛出SignatureException。如果在做 JWT 解析时,发生了验证失败的现象,可以通过捕获JwtException异常来自定义失败情况下的处理
1 | try { |
¶JWS
JWS 就是已经签名的 JWT,下面展示了如何手动实现 JWS
1 | //头部信息 |
以上就是 JWS(已经签名的 JWT)生成过程的实现方式
¶创建 JWS
可以使用这种方式创建JWS:
- 使用
Jwts.builder()方法创建JwtBuilder实例 - 调用
JwtBuilder方法根据需要添加标头参数和声明 - 指定要用于对
JWT进行签名的SecretKey或非对称PrivateKey - 调用
compact()方法进行压缩和签名,生成最终的jws
1 | String jws = Jwts.builder() // (1) |
¶设置 Header Parameters
JWT Header提供关于JWT Claims相关的内容,格式和加密操作的元数据
如果需要设置一个或多个JWT头参数,则可以根据需要简单地多次调用JwtBuilder.setHeaderParam
1 | String jws = Jwts.builder() |
每次调用setHeaderParam时,它只是将键值对附加到内部Header实例,如果键值已经存在,则会覆盖任何现有的同名键/值对
除此之外,还可以使用其他方式,设置JWT Header,由于第一种方式能够完成常见的业务场景,其余的方式这里并不阐述
¶设置 Claims
Claims是JWT的正文部分,包含JWT创建者希望向JWT收件人提供的信息,常见的 API 如下
| API | 含义 |
|---|---|
setIssuer | sets the iss (Issuer) Claim |
setSubject | sets the sub (Subject) Claim |
setAudience | sets the aud (Audience) Claim |
setExpiration | sets the exp (Expiration Time) Claim |
setNotBefore | sets the nbf (Not Before) Claim |
setIssuedAt | sets the iat (Issued At) Claim |
setId | sets the jti (JWT ID) Claim |
1 | String jws = Jwts.builder() |
当然也可以自定义 Claims,如果需要设置一个或多个与上面显示的标准 setter 方法声明不匹配的自定义声明,可以根据需要多次调用JwtBuilde.claim 声明:
1 | String jws = Jwts.builder() |
每次调用claim时,它只是将键值对附加到内部claims实例,如果键值已经存在,则会覆盖任何现有的同名key/value对,同头部信息一样
¶签名
JWT 规范确定了 12 种标准签名算法,三种密钥算法和九种非对称密钥算法,由于也不是做密码学的这里也不过多展开。自己项目中常见的使用HMAC-SHA-256就足够了
👴建议通过调用JwtBuilder的signWith方法来指定签名密钥,以及对应的摘要算法,示例如下:
1 | String jws = Jwts.builder() |
使用signWith时,JJWT还会自动使用相关的算法标识符设置所需的alg头。类似地,如果使用长度为 4096 位的RSA PrivateKey调用signWith,JJWT将使用RS512算法并自动将alg头设置为RS512
¶解析 JWS
⛵解析 JWS 步骤如下:
- 使用
Jwts.parser()方法创建JwtParser实例 - 指定要用于验证
JWS签名的SecretKey或非对称PublicKey - 调用
parseClaimsJws(String)方法,生成原始JWS
🎶如之前所述,整个调用将包装在try/catch块中,以防解析或签名验证失败
1 | Jws<Claims> jws; |
¶校验 Key
阅读JWS时,最重要的事情是指定用于验证 JWS 加密签名的密钥。 如果签名验证失败,则无法安全地信任此JWT,应将其丢弃。如果jws是使用SecretKey签名的,则应在JwtParser上指定相同的SecretKey
1 | Jwts.parser() |
如果jws是使用PrivateKey签名的,那么应该在JwtParser上指定该密钥相应的PublicKey(不是PrivateKey)
1 | Jwts.parser() |
❓如果应用程序不止使用一个 SecretKey 或 KeyPair 会怎么样? 如果可以使用不同的 SecretKeys 或公钥/私钥或两者的组合创建 JWS,该怎么办?
1 | //实现了 SigningKeyResolver 接口的自定义解析类 |
通过实现SigningKeyResolverAdapter接口的resolveSigningKey(JwsHeader,Claims)方法来简化一些事情
1 | public class MySigningKeyResolver extends SigningKeyResolverAdapter { |
在解析JWS JSON之后,JwtParser将在验证jws签名之前调用resolveSigningKey()方法。 这也就允许检查Jws Header和Claims参数,以帮助查找用于验证特定jws的密钥的信息。 这对于复杂安全模型的应用程序非常强大,这些安全模型可能在不同时间使用不同的密钥或针对不同的用户或客。JWT规范支持的方法是在创建JWS时,在JWS头中设置kid(Key ID)字段,例如:
1 | Key signingKey = getSigningKey(); |
然后在解析期间,SigningKeyResolver可以检查JwsHeader以获取该kid,然后使用该值从某个位置查找密钥,如数据库。 例如:
1 | public class MySigningKeyResolver extends SigningKeyResolverAdapter { |
🎶检查jwsHeader.getKeyId()只是查找密钥的最常用方法,也可以检查任意数量的标头字段或声明,以确定如何查找验证密钥。
¶Claims 断言
如果正在解析的JWS具有特定的子sub值,可以使用JwtParser上的各种require *方法来获取数据
1 | try { |
如果缺少某个值,那么就不会捕获InvalidClaimException,而是捕获MissingClaimException或IncorrectClaimException异常
1 | try { |
当然,也可以使用require(fieldName,requiredFieldValue)方法来获取自定义字段。
1 | try { |
¶压缩
如果JWT的Claim域可以足够大,包含许多key/value对;或者单个值非常冗长,可以通过压缩来减小创建的JWS的大小
¶默认压缩
如果要压缩JWT,可以使用JwtBuilder 的compressWith(CompressionAlgorithm)方法。 例如:
1 | Jwts.builder() |
使用DEFLATE或GZIP压缩编解码器,但是在解压缩时,不必执行任何操作,不需要配置JwtParser,JJWT将按预期自动解压缩主体
¶自定义压缩
如果在创建JWT时使用自己的自定义压缩编解码器(通过JwtBuilder.compressWith),则需要使用setCompressionCodecResolver方法将编解码器提供给JwtParser。 例如:
1 | CompressionCodecResolver ccr = new MyCompressionCodecResolver(); |
通常,CompressionCodecResolver实现将检查zip标头以找出使用的算法,然后返回支持该算法的编解码器实例。 例如:
1 | public class MyCompressionCodecResolver implements CompressionCodecResolver { |
