jwt说明
JWT(Json Web Token),遵循json格式,用于保存用户信息,存放于客户端;用户请求数据时,带上此token进行身份验证。jwt中的数据基本上是明文传输,本身不能提供网络安全保障。
JWT的结构,用符号“.”将之分为三部分,header、payload和signature。
- header:包含jwt的配置信息,例如签名加密算法(alg),类型(默认为JWT),算法密钥信息(kid);可以写入自定义属性,格式为json;
- payload:token的实体部分,用来保存需要传递的数据;协议中规定了几个字段,iss(签发人),exp(token过期时间),sub(主题),adu(受众),nbf(生效时间),iat(签发时间),jti(编号),可以写入自定义属性,格式为json;
- signature:签名部分主要用来进行数据完整性校验,非必须;签名的算法比较简单,
signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret )
jwt生成
java环境需要添加maven依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.1</version>
</dependency>
生成的token中,如果不需要签名【程序调试阶段】,使用:Algorithm.none();如果需要用HS256对称加密算法生成签名,则使用:Algorithm.HMAC256("secret-code");其中secret-code是对称加密的密码。jwt还提供RS256和ES256非对称加密算法来生成签名。
- 注意此处的加密说明,虽然文档中提及了对称加密,但是经过多次验证,无论是2.2.0版本的【new JWTSigner(SECRET).sign(claims)】,还是当前的3.8.1版本,都可以用Base64.decode()的方式,直接解码出来。
- 加密了,但是没有完全加密【dog】
下面代码展示了生成token的过程,其中header和payload都可以自定义属性。
@Test
public void testPayload(){
Map<String, Object> headers = new HashMap<>();
headers.put("head_user", "tom");
headers.put("contextType", "json");
headers.put("alg", "HS256");
headers.put("typ", "JWT");
// 如果用Algorithm.none(),则表示不生成签名 "alg":"none"
// Algorithm algorithm = Algorithm.none();
// 生成签名,"alg":"HS256",密钥为:secret-code
Algorithm algorithm = Algorithm.HMAC256("secret-code");
JWTCreator.Builder builder = JWT.create()
// head数据定义 ===============================================
// head部分的kid属性
.withKeyId("key_233221")
// head部分定义其他属性
.withHeader(headers)
// payload数据定义 ============================================
// 签发人
.withIssuer("clientId")
// 主题
.withSubject("主题")
// 受众
.withAudience("观众1","观众2")
// 生效时间
.withNotBefore(new Date(System.currentTimeMillis() + 10 * 1000))
// 签发时间
.withIssuedAt(new Date(System.currentTimeMillis()))
// 过期时间
.withExpiresAt(new Date(System.currentTimeMillis() + 100 * 1000))
// 编号
.withJWTId("jwt_id_111")
// 声明自定义属性
.withClaim("test", 23333)
.withClaim("userId", 1003)
// 声明自定义数组属性
.withArrayClaim("auditor", new String[]{"张三", "李四", "Shierly"})
;
String token = builder.sign(algorithm);
// 生成的token
System.out.println(token);
}
上面这段代码生成的token:
eyJraWQiOiJrZXlfMjMzMjIxIiwiY29udGV4dFR5cGUiOiJqc29uIiwidHlwIjoiSldUIiwiaGVhZF91c2VyIjoidG9tIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiLkuLvpopgiLCJhdWQiOlsi6KeC5LyXMSIsIuinguS8lzIiXSwibmJmIjoxNTk3ODIwNDA5LCJ0ZXN0IjoyMzMzMywiaXNzIjoiY2xpZW50SWQiLCJhdWRpdG9yIjpbIuW8oOS4iSIsIuadjuWbmyIsIlNoaWVybHkiXSwiZXhwIjoxNTk3ODIwNDk5LCJpYXQiOjE1OTc4MjAzOTksInVzZXJJZCI6MTAwMywianRpIjoiand0X2lkXzExMSJ9.9KH6EoPqSC_g9LX9JM-tbjNKlBFjvZaaWKocOykv6HM
前文有交代,jwt是明文传输的,这里用代码解析一下刚刚生成的token:
@Test
public void testRead() {
// header部分
byte[] bytes = Base64Utils.decodeFromUrlSafeString("eyJraWQiOiJrZXlfMjMzMjIxIiwiY29udGV4dFR5cGUiOiJqc29uIiwidHlwIjoiSldUIiwiaGVhZF91c2VyIjoidG9tIiwiYWxnIjoiSFMyNTYifQ");
System.out.println(new String(bytes));
// payload部分
bytes = Base64Utils.decodeFromUrlSafeString("eyJzdWIiOiLkuLvpopgiLCJhdWQiOlsi6KeC5LyXMSIsIuinguS8lzIiXSwibmJmIjoxNTk3ODE4NzgxLCJ0ZXN0IjoyMzMzMywiaXNzIjoiY2xpZW50SWQiLCJhdWRpdG9yIjpbIuW8oOS4iSIsIuadjuWbmyIsIlNoaWVybHkiXSwiZXhwIjoxNTk3ODE4ODcxLCJpYXQiOjE1OTc4MTg3NzEsInVzZXJJZCI6MTAwMywianRpIjoiand0X2lkXzExMSJ9");
System.out.println(new String(bytes));
}
输出信息
{"kid":"key_233221","contextType":"json","typ":"JWT","head_user":"tom","alg":"HS256"}
{"sub":"主题","aud":["观众1","观众2"],"nbf":1597818781,"test":23333,"iss":"clientId","auditor":["张三","李四","Shierly"],"exp":1597818871,"iat":1597818771,"userId":1003,"jti":"jwt_id_111"}
jwt校验
这里主要校验token的完整性,防止数据被篡改。
校验的代码
@Test
public void testVerifySecret(){
String token2 = "eyJraWQiOiJraWQtMSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJjbGllbnRJZCIsInVzZXJJZCI6MTAwMywidXNlcm5hbWUiOiLlvKDkuIkifQ.GuesgTUyP6InsY6dLIq1Kc8zsb8WCLxGw2zgWL5i2Hc";
// 修改密码,The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256
Algorithm algorithm = Algorithm.HMAC256("secret-code");
// 这里列举需要校验的属性,如果没有列出,则不需要比较
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("clientId")
.withClaim("userId", 1003)
.build();
DecodedJWT jwt = verifier.verify(token2);
System.out.println(new String(Base64Utils.decodeFromString(jwt.getHeader())));
System.out.println(new String(Base64Utils.decodeFromString(jwt.getPayload())));
}
jwt校验源代码
/**
* jwt校验源码
* @param jwt token的解码对象,DecodedJWT jwt = new JWTDecoder(parser, token)
* @return DecodedJWT 返回方法的参数
*/
public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException {
verifyAlgorithm(jwt, algorithm);
algorithm.verify(jwt);
verifyClaims(jwt, claims);
return jwt;
}
这里简单说一下校验过程
- 校验签名算法是否一致,verifyAlgorithm(jwt, algorithm);
- 用服务端的密钥重新计算签名,和token参数中签名进行比较,如果不一致,校验失败;algorithm.verify(jwt);如果服务端的密钥泄露,则这一步的校验则无任何意义。
- 比较JWTVerifier对象中列举的属性,如果属性值不一致,则校验失败;
补充说明:
第二步:如果密钥泄露,那么还有第三步勉强挽救一下,比如校验通过了第二步,在第三步时,userId和服务端数据库中保存的不一致,那么也会校验失败。
第三步:这里不会比较header中的属性,也不会比较JWTVerifier对象中没有列举的属性;比如我们的token中有username属性,但是JWTVerifier只列举了Issuer和userId【上述的例子】,则username属性不参与校验。