代码部分
代码部分内容全部默认使用JJWT,且参考于JJWT GitHub仓库的自述文件(readme.md)
快速开始
这里使用封装好的JJWT进行JWT的生成和解密
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<!-- jdk11以上注释bcprov-jdk15on依赖-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version>
<scope>runtime</scope>
</dependency>
JWT生成
因为这里使用了builder(建造者模式)的原因,复杂的操作全部都在这两行简单代码之下
//快速开始
//签名使用加密算法设置
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
//sub 可达鸭
String compact = Jwts.builder().setSubject("可达鸭").signWith(secretKey).compact();
System.out.println(compact);
//运行结果:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlj6_ovr7puK0ifQ.9YyUs3TafLrJoXmBwMswFHW_ycQkTlhqaKti4Wh2aoM
//签名部分与我不同属于正常现象
这里我们都做了些什么呢?
1.创建一个JWT并设置sub值为可达鸭
2.使用适用于HmacSHA256算法的密钥对JWT进行签名
3.最后将其压缩成一个String,一般被签名的JWT也被叫做JWS
JWT验证
//jwt解码器建造工厂 设置jwt解码器签名加密方式 建造 用此解码器验证jwt(exception则表示jwt不合法或者签名加密方式不对)
//获取jwt payload(body)部分 获取subject
String subject = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(compact).getBody().getSubject();
System.out.println(subject);
//运行结果 可达鸭
需要注意,如果签名(secretKey)有误,则会产生SignatureException,也可直接处理JwtException
完整步骤
创建JWT
创建JWT的步骤如下:
1.使用Jwts.builder()方法创建JwtBuilder实例
2.调用JwtBuilder方法(setHeaderParam/claim)以根据需要添加标头参数和声明
3.指定要用于对JWT进行签名的SecretKey或不对称PrivateKey
4.最后,调用compact()方法进行压缩和签名,生成最终的jwt。
例:
String jwt = Jwts.builder() //构建Jwt建造者
.setHeaderParam("typ", "JWT") //设置标头参数
.claim("hello", "world") //设置索偿(payload jwt主体内容)
.claim("name", "可达鸭")
.signWith(secretKey) //设置加密使用的密钥及加密算法(加密算法可不指定)
.compact();
标头参数
JWT标头应当写明有关JWT的格式和加密操作的相关数据。
如果需要设置一个或多个JWT标头参数,例如kid (Key ID)标头参数,则可以使用setHeaderParam方法根据需要调用 一次或多次。
例:
String jwt = Jwts.builder() //构建Jwt建造者
.setHeaderParam("typ", "JWT") //设置标头参数
//.setHeaderParam("kid","myKey") 等等
注意:调用setHeader如果key值相等会出现覆盖标头参数的情况,任何情况下,JJWT都会覆盖已经被设置好的alg和zip标头。
载荷
JWT的载荷用于存储需要的数据,可以通过调用一次或多次claim存储载荷,也可以通过setClaims直接存储一个Map进去
例:
Map<String,Object> testMap = new HashMap<String, Object>(){
{
put("hello","hashMap");
put("name","psyDuck");
}
};
String jwt = Jwts.builder() //构建Jwt建造者
.setHeaderParam("typ", "JWT") //设置标头参数
.claim("hello", "world") //设置索偿(payload jwt主体内容)
.claim("name", "可达鸭")
.setClaims(testMap)//存入HashMap键值对
注意:调用claim/setClaims如果key值相等会出现覆盖载荷参数的情况,在调用setClaims时会清空所有使用claim存入的值。
签名
我们可以通过以下代码自动生成一个随机的key
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //也可以是HS384或HS512等等等等
如果需要保存这个Key,可以使用以下代码
String secretString = Encoders.BASE64.encode(key.getEncoded());
之后对已经设置好标头和载荷的JWT进行加密,加密算法会自动选择一个最适合的使用。
String jwt = Jwts.builder() //构建Jwt建造者
.setHeaderParam("typ", "JWT") //设置标头参数
.claim("hello", "world") //设置索偿(payload jwt主体内容)
.claim("name", "可达鸭")
.setClaims(testMap)//存入HashMap键值对
.signWith(secretKey) //设置加密使用的密钥及加密算法(加密算法可不指定)
.compact();
加密结束之后通过.compact完成JWT的生成。
解析JWT
相对于创建JWT,解析同样用到了Builder(建造者模式),我们只需要提供加密时所提供的密钥,就可以构建一个可重复使用的JWT解析器用以解析JWT。
//读取JWT
String s = new String(Base64.encodeBase64URLSafe(secretKey.getEncoded()));
System.out.println(s);
JwtParser jwtParser = Jwts.parserBuilder() //构建JWT解析器建造者
.setSigningKey(s) //指定要解析的JWT所使用的secretKey用以判断签名是否正确 不正确代表JWT已不可信(会抛出异常)
.build(); //调用build来返回线程安全的JWT解析器
//填入需要解析的JWT
Jws<Claims> jws = jwtParser.parseClaimsJws(jwt);
//header={typ=JWT, alg=HS256},body={name=psyDuck, hello=hashMap},signature=9tT7x5dvEbjr1KffJy9r-r1sOspJ5Jj83swCyjK2ERc
System.out.println(jws);
理论部分
什么是JWT规范?
JWT规范将一个token分为3部分,分别是标头(header)、载荷(payload)、签名(verify signature)三部分并以.进行分割,也就是说一个符合JWT规范所生成的token格式应当如下:
xxxxx.yyyyy.zzzzz
tip:JWT官网:https://jwt.io/
标头(header)
标头首先是一个JSON,通常包含两部分。分别是代表令牌类型和所使用的签名的加密算法的typ和alg,
例:
{
"alg": "HS256",
"typ": "JWT"
}
之后将此JSON以Base64URL加密作为标头
JWT推荐使用的加密方式
HS256、HS384、HS512,
RS256、RS384、RS512,
ES256、ES384、ES512,
PS256、PS384;
tip:大部分JWT的封装库还另外实现了ES256K、PS512、EdDSA等数种加密方式
载荷(payload)
载荷部分说的通俗一点就是你希望传递的信息部分,这部分开始也是一个JSON,JWT规范将JSON的key叫做索赔(Claim)。
索赔分为3种,已注册的、公开的、私人的
已注册的索赔是JWT在IANA“ JSON Web令牌声明”注册表中注册过的,
公开的索赔是由我们,也就是开发人员随意定义的,
私人的索赔是由JWT的生产者、消费者所共同定义的索赔,
需要注意的是,私人索赔更易发生冲突,应谨慎使用,同时索赔最好只有3个字符,因为JWT是紧凑(compact)的。
例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
之后将此JSON以Base64URL加密作为载荷。
已注册的索赔
请注意,已注册的索赔全部为可用可不用,并不一定要使用以下索赔
名称 | 备注 |
---|---|
iss(发布者) | 其值为区分大小写的字符串或StringOrURI |
sub(主题) | 该值应当是局部或全局唯一的,且是区分大小写的字符串或StringOrURI |
aud(收听者) | 其值为区分大小写的字符串数组,每个字符串都是StringOrURI,如果只有一个收听者的情况下,其值可以仅为一个StringOrURI |
exp(到期时间) | 其值应当为一个NumericDate,如果处理此JWT的日期晚于exp,则弃用此JWT |
nbf(不早于) | 其值应当为一个NumericDate,如果处理此JWT的日期早于nbf,则弃用此JWT |
iat(签发时间) | 其值应当为一个NumericDate,用以说明此JWT生成的时间 |
jti(JWT ID) | 其值应当为一个区分大小写的字符串,主要用来规避重放攻击,需要考虑多个发布者和收听者时可能出现的冲突 |
通俗解释名词
StringOrURI:JSON数据
NumericDate:时间戳
签名(verify signature)
JWT的签名首先需要你随机生成一个密钥,例:
String secret = "123456";//普通密钥
secret = new String(Base64.encodeBase64URLSafe("123456".getBytes("UTF-8")));//Base64加密密钥
//......各种特殊加密密钥
密钥可以被接受的几种格式如下
//key值至少应为32个可见数字 也就是 32*8=256位 否则Keys会因为key值位数不够抛出一个异常
String key = "11111111111111111111111111111111";
// 密钥可以为 一个原始字符串
// SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes());
// 密钥可以为 一个byte数组
// SecretKey secretKey = Keys.hmacShaKeyFor(new byte[]{
// 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17
// ,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17
// });
//密钥可以为 一个Base64编码后的String
//在使用BASE64编码key时,相同数字会被压缩 所以我们需要提供额外X个可见数字
// key += "34567891011";
// SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(key));
//密钥可以为 一个Base64URL编码后的String 同样需要额外提供X个可见数字
// key += "34567891011";
// SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(key));
之后将标头(已base64url加密的)和载荷(已base64url加密的)用.拼接成一个完整的字符串作为message
将生成的密钥作为secret进行标头中设置好的加密方式的加密,默认是HS256的话也就是进行一次HmacSHA256加密。
因为这个签名始终是一个byte类型的数组,所以我们需要对其进行base64URL编码使其变成一个正常的字符串,不过简单看一下jjwt的源码,jjwt在最终获取key时已经进行了一次base64编码,所以我们可以直接使用其作为签名。
public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty.");
Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.");
SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());
return this.signWith((Key)key, (SignatureAlgorithm)alg);
}
public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException {
Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty.");
Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.");
byte[] bytes = (byte[])Decoders.BASE64.decode(base64EncodedSecretKey);
return this.signWith(alg, bytes);
}
最后更新于2021年5月28日
原创不易,如果该文章对你有所帮助,望左上角点击关注~如有任何技术相关问题,可通过评论联系我讨论,我会在力所能及之内进行相应回复以及开单章解决该问题.
该文章如有任何错误请在评论中指出,感激不尽,转载请附出处!
*个人博客首页:https://blog.youkuaiyun.com/yjrguxing ——您的每个关注和评论都对我意义重大