1 JWT 介绍
1.1 JWT组成
JWT 是JSON Web Token的缩写。 是由3部分组成的,他们之间使用英文句号 . 分割。
大概长得下面这样子:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODkzODE5Mzg4MTAsInBheWxvYWQiOiJcImFiY1wiIn0.EZNHd_YcnXqhEd0Ol-ilPaY_c2c8115DRGJJIUCDrAA
3部分分别是:
- header(头部)
- payload(负载)
- signature(签名)
所以JWT中具体的表现形式就是:Base64url(header) + '.' + Base64url(payloay) + '.' + signature
1.1.1 Header
Header 是一个JSON格式的字符串,标准定义中包括2个部分:
- agl: 消息认证的算法,也就是第三部分signature使用的算法。通常使用的是HS256( HMAC with SHA-256 对称加密算法)或者RS256(RSA signature with SHA-256 非对称加密算法)。
- typ: 令牌的类型,既然是JWT,那这部分基本都是JTW了
所以JWT Header长得如下:
{
"alg" : "HS256",
"typ" : "JWT"
}
1.1.2 Payload
Payload是一个JSON格式的字符串,标准定义中包括7个部分:
code | name | description |
| Issuer | 发布JWT的主体,也就是签发者,发布者 |
| Subject | JWT的主题 |
| Audience | JWT要发送给的收件人 |
| Expiration Time | 过期时间戳 |
| Not Before | JWT 生效时间戳 |
| Issued at | 签发时间戳 |
| JWT ID | JWT ID编号 |
官方标准中的字段不强求一定使用,甚至可以不定义其中任何字段,除了标准定义中的7个字段,当然可以自定义字段。例如添加用户的一些信息"clientId":"xxxx"
所以Payload长得如下:
{
"iss":"abc.com"
"sub": "xxxx",
"clientName": "Allen",
"clientId": "11233"
}
需要注意的是:Header 和payload都仅仅是Base64url编码,而不是加密,所以基本是不允许放置敏感信息在其中的。因为编码是随意都可以解码成为明文的。
1.1.3 Signature
签名是通过使用Base64编码对Header和Payload进行编码,并使用句点分隔符将两者连接在一起来计算的。然后,该字符串通过Header中指定的加密算法运行。这里说到加密,就肯定涉及到秘钥。所以signature包含3部分的信息
- Base64url(header)
- Base64url(payload)
- secret
使用在Header中指定的算法,用secret对base64(header) + '.' + base64(payloay),进行加密操作。 这个过程可以理解为签名。
signature经常使用的方式长得如下:HS256 接收两个参数(encryp string, secret)
HMAC-SHA256(
base64urlEncoding(header) + '.' + base64urlEncoding(payload),
secret
)
算出签名之后,把三部分使用句点分隔符连接起来就可以返回给用户了。长得就和开篇展示的那样。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI
1.2 JWT在微服务(springboot)的运用
在微服务中,我们会使用JWT来作为权限校验的一个工具。通常会在HTTP HEADER 中设置一个Authorization 的字段信息,通常对于springboot 项目,长得如下:
Authorization: Bearer <JWT string>
具体流程大概如下:
- 首次登录APP或者首页时,访问后端getAuth 接口,带上用户相应信息(假设已经是注册的用户)
- 后端系统返回JWT,对于微服务来说,这些工作一般都在网关服务中进行操作。
- 每次请求任何API都会在HTTP HEADER的Authorization字段带上这个JWT
- 后端系统获取到JWT后,先做签名校验,使用指定的算法,用secret对base64urlEncoding(header) + '.' + base64urlEncoding(payload),签名如果相等,就表示JWT Header 和Payload没有被篡改过;然后Payload如果定义了exp,那么就要校验exp是否过期,如果过期,API 请求是吧;否则,请求成功。如果没有定义exp 字段,则可以正常访问API逻辑,可以获取API数据
- 一般作为API校验的都不会使用exp 来作为JWT 是否过期的标识,这样可以避免正常的访问需要频繁更新JWT。一般会试用一些辅助的方式来校验请求的合理性和合法性。例如在Payload中定义clientId,然后使用缓存工具(redis,memcache)来保存clientId的相关信息,如果在缓存中正常获取到的clientId对应的信息,则是用户的合法请求,这时候可以对缓存的信息进行续期;否则则说明用户信息已经失效,需要重新登录操作才能对API 进行请求操作。
简单的请求流程如下:
附上JWT 使用的主要方法:
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWTSigner;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.JWTVerifyException;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.HashMap;
import java.util.Map;
public class JWT {
private static final String SECRET = "123)sdfose";
private static final String EXP = "exp";
private static final String PAYLOAD = "payload";
/**
* 将对象签名为JWT的token字符串,(指定有效时间)
*
* @param payloadObj
* @param maxAge
* @return the jwt token
*/
public static <T> String sign(T payloadObj, long maxAge) {
try {
assert payloadObj != null;
final JWTSigner signer = new JWTSigner(SECRET);
final Map<String, Object> claims = new HashMap<>(2);
claims.put(EXP, System.currentTimeMillis() + maxAge);
claims.put(PAYLOAD, JSON.toJSONString(payloadObj));
return signer.sign(claims);
} catch (Exception e) {
return null;
}
}
/**
* valid the JWT
*
* @param jwtToken
* @return
*/
public static boolean validate(String jwtToken){
final JWTVerifier verifier = new JWTVerifier(SECRET);
Map<String, Object> claims;
try {
claims = verifier.verify(jwtToken);
if(claims == null || claims.isEmpty()){
return false;
}
if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)){
long exp = (Long) claims.get(EXP);
long currentTimeMillis = System.currentTimeMillis();
if (exp > currentTimeMillis) {
//有效!
return true;
}else{
//过期了!
return false;
}
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JWTVerifyException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return false;
}
/**
* 从jwt token签名中获取加密前的对象。<br>
* 注意,如果过了有效期或者解密密钥有错误,本方法会吃掉异常,返回值为null~!
*
* @param jwt
* token
* @return POJO object 解密后的对象或者null
*/
public static <T> T unsign(String jwt, Class<T> clazz) {
final JWTVerifier verifier = new JWTVerifier(SECRET);
try {
final Map<String, Object> claims = verifier.verify(jwt);
if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) {
long exp = (Long) claims.get(EXP);
long currentTimeMillis = System.currentTimeMillis();
if (exp > currentTimeMillis) {
return JSON.parseObject(claims.get(PAYLOAD).toString(), clazz);
}
}
return null;
} catch (Exception e) {
return null;
}
}
/**
* test
* @param args
*/
public static void main(String[] args) {
String abc = sign("abc", 10000);
System.out.println(abc);
String unsign = unsign(abc, String.class);
System.out.println(unsign);
}
}
参考资料:
维基百科:https://en.wikipedia.org/wiki/JSON_Web_Token
JSON Web Token 入门教程:https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html