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

iss

Issuer

发布JWT的主体,也就是签发者,发布者

sub

Subject

JWT的主题

aud

Audience

JWT要发送给的收件人

exp

Expiration Time

过期时间戳

nbf

Not Before

JWT 生效时间戳

iat

Issued at

签发时间戳

jti

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>

具体流程大概如下:

  1. 首次登录APP或者首页时,访问后端getAuth 接口,带上用户相应信息(假设已经是注册的用户)
  2. 后端系统返回JWT,对于微服务来说,这些工作一般都在网关服务中进行操作。
  3. 每次请求任何API都会在HTTP HEADER的Authorization字段带上这个JWT
  4. 后端系统获取到JWT后,先做签名校验,使用指定的算法,用secret对base64urlEncoding(header) + '.' + base64urlEncoding(payload),签名如果相等,就表示JWT Header 和Payload没有被篡改过;然后Payload如果定义了exp,那么就要校验exp是否过期,如果过期,API 请求是吧;否则,请求成功。如果没有定义exp 字段,则可以正常访问API逻辑,可以获取API数据
  5. 一般作为API校验的都不会使用exp 来作为JWT 是否过期的标识,这样可以避免正常的访问需要频繁更新JWT。一般会试用一些辅助的方式来校验请求的合理性和合法性。例如在Payload中定义clientId,然后使用缓存工具(redis,memcache)来保存clientId的相关信息,如果在缓存中正常获取到的clientId对应的信息,则是用户的合法请求,这时候可以对缓存的信息进行续期;否则则说明用户信息已经失效,需要重新登录操作才能对API 进行请求操作。

简单的请求流程如下:

springboot jodconverter全部配置_字段

附上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