JWT是基于json制作的一个web token的一套规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,它属于一种无状态的数据,它主要有两大使用场景:认证和数据传递。
JWT的内容如下:
eyJhbGciOiJIUzUxMiJ9.eyJwYXNzd29yZCI6IjQ1NiIsImV4cCI6MTY2MDUyOTI0MCwiaWF0IjoxNjYwNTI3NDQwLCJ1c2VybmFtZSI6InpoYW5nc2FuIn0.LbtbH4DRn7W8PAzre1Cflpfg3W9grzPQIs7Z_86q5J9Gm8YiRxvBjZ7gPmils6LBzvmFKmvcgywwHELLPlCQKQ
由上面的JWT的内容我们根据“.”这个字符把内容隔开看,内容一共分为三段,如下
eyJhbGciOiJIUzUxMiJ9
eyJwYXNzd29yZCI6IjQ1NiIsImV4cCI6MTY2MDUyOTI0MCwiaWF0IjoxNjYwNTI3NDQwLCJ1c2VybmFtZSI6InpoYW5nc2FuIn0
LbtbH4DRn7W8PAzre1Cflpfg3W9grzPQIs7Z_86q5J9Gm8YiRxvBjZ7gPmils6LBzvmFKmvcgywwHELLPlCQKQ
这三段内容也就是jwt的主要组成部分,看到这三段内容,我们肯定会不由自主的想到,这三段内容到底是什么呢?有什么作用呢?安全起见为什么一定是三段呢?
header里面存放的是一个内容包含了算法名称已经类型的json对象,然后以base64处理之后的数据。
payLoad 里面存放的是user的数据的json对象,然后以base64处理之后的数据。
signature 里面存放的是前面两个的数据组合起来,然后加上一个加密盐通过第一段设置的加密方式加密起来,然后以base64处理的数据。
JWT的生成token以及token的解析代码如下:
package com.zw.jwtdemo.util;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class JwtUtil {
private String keyScret ="123456";
private int expiryTime = 30;
public String getToken(Map claims) {
if(claims == null || claims.size() == 0) {
return null;
}
Date createTime = new Date();
Calendar calander = Calendar.getInstance();
calander.setTime(createTime);
calander.add(Calendar.MINUTE, expiryTime);
return Jwts.builder()
//payload
//设置携带的内容即数据信息。
.setClaims(claims)
//设置过期时间
.setExpiration(calander.getTime())
//设置创建时间
.setIssuedAt(createTime)
//设置加密盐和签名算法
.signWith(SignatureAlgorithm.HS512, keyScret)
//数据压缩得到一个字符串
.compact();
}
public Map parseToken(String token) {
Claims claims = Jwts.parser()
//设置加密盐
.setSigningKey(keyScret)
//通过压缩的jwt
//字符串解析出数据信息
.parseClaimsJws(token)
//返回数据信息;
//返回的内容是一个hashMap
.getBody();
return claims;
}
public static void main(String[] args) {
Map map = new HashMap(); JwtUtil jwtUtil = new
JwtUtil(); map.put("username", "zhangsan"); map.put("password", "456");
String token =jwtUtil.getToken(map); System.out.println(token);
Map map2 = jwtUtil.parseToken(token); Set keys =
map2.keySet(); for(String key: keys) { System.out.println(key + "=" +
String.valueOf(map2.get(key))); }
}
}
//输出结果
eyJhbGciOiJIUzUxMiJ9.eyJwYXNzd29yZCI6IjQ1NiIsImV4cCI6MTY2MTE1MjkyOCwiaWF0IjoxNjYxMTUxMTI4LCJ1c2VybmFtZSI6InpoYW5nc2FuIn0.BC-M5R9kThhW1H7DF0c5OLczQyV6xSOc6Sj3kS2bU67dpjGUmCcMOyFF9H2x0mK7z6J4zqzsB0Z0uJ48CGtDqw
password=456
exp=1661152928
iat=1661151128
username=zhangsan
上面的代码段便是JWT的代码段了,代码比较简单生成的JWT也是通过“.”隔开的三段字符串拼接而成。接下来我没还是带着我之前提出的三个问题,分析下源码。
JWT加密的源码主要就是compact方法,我们进入到compact方法看看,这个方法里面到底有啥内容。
@Override
public String compact() {
//payload和claims二者必须要有一个
if (payload == null && Collections.isEmpty(claims)) {
throw new IllegalStateException("Either 'payload' or 'claims' must be specified.");
}
//payload和claims二者只能有一个
if (payload != null && !Collections.isEmpty(claims)) {
throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one.");
}
//加密盐的key和字符数组只能有一个,防止两个不同的key同时用来加密一个JWT
if (key != null && keyBytes != null) {
throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either one.");
}
//设置JWT header
Header header = ensureHeader();
//设置加密key
Key key = this.key;
if (key == null && !Objects.isEmpty(keyBytes)) {
key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
}
JwsHeader jwsHeader;
if (header instanceof JwsHeader) {
jwsHeader = (JwsHeader)header;
} else {
jwsHeader = new DefaultJwsHeader(header);
}
//设置加密算法 存放在头里面
if (key != null) {
jwsHeader.setAlgorithm(algorithm.getValue());
} else {
//no signature - plaintext JWT:
jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());
}
//设置JWT的压缩方式
if (compressionCodec != null) {
jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName());
}
//把header里面的内容通过base64的方式压缩得到header的字符串
String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json.");
String base64UrlEncodedBody;
//如果压缩方式不为空
if (compressionCodec != null) {
byte[] bytes;
try {
//讲数据信息体转成json的字符串,然后再将字符串转成转成字符数组。
bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Unable to serialize claims object to json.");
}
//将字符数组通过指定的压缩方式进行压缩 然后进行base64处理的带一个payload的字符串数据体
base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes));
} else {
//如果没有指定压缩方式,就会直接把数据信息体转成json字符串然后转成字符数据,直接通过base64处理得到一个payload的字符串
base64UrlEncodedBody = this.payload != null ?
TextCodec.BASE64URL.encode(this.payload) :
base64UrlEncode(claims, "Unable to serialize claims object to json.");
}
//再将头字符串和payload字符串通过“.”拼接起来得到一个初步的JWT字符串
String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody;
if (key != null) { //jwt must be signed:
//通过设置的加密算法和加密盐设置数字签名
JwtSigner signer = createSigner(algorithm, key);
//通过加密签名把base64处理之后的头和base64处理之后的payload以及“.”组合起来的字符串进行加密。得到一个加密的字符串
String base64UrlSignature = signer.sign(jwt);
//把base64处理之后的头和base64处理之后的payload以及“.”组合起来的字符串再拼接上“.”以及数字签名之后得到的base64处理过的字符串。整个一起构成了一个字符串即是完成的JWT
jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature;
} else {
// no signature (plaintext), but must terminate w/ a period, see
// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1
//如果不需要后面的数字签名部分的话,就只是返回前面两端的内容并且后面拼上一个“.”
jwt += JwtParser.SEPARATOR_CHAR;
}
return jwt;
}
@Override
public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {
//判断传过来的JWT的数据不能为空
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
//JWT的Header的base64的字符串
String base64UrlEncodedHeader = null;
//JWT的payload的base64的字符串
String base64UrlEncodedPayload = null;
//JWT的数字签名的base64的字符串
String base64UrlEncodedDigest = null;
int delimiterCount = 0;
StringBuilder sb = new StringBuilder(128);
for (char c : jwt.toCharArray()) {
//以“.”作为分割线,来分割得到JWT的3个部署的字符串
if (c == SEPARATOR_CHAR) {
CharSequence tokenSeq = Strings.clean(sb);
String token = tokenSeq!=null?tokenSeq.toString():null;
if (delimiterCount == 0) {
base64UrlEncodedHeader = token;
} else if (delimiterCount == 1) {
base64UrlEncodedPayload = token;
}
delimiterCount++;
sb.setLength(0);
} else {
sb.append(c);
}
}
//如果JWT的字符串里面没有两个“.”表示不是规范的JWT的内容,抛出异常
if (delimiterCount != 2) {
String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
throw new MalformedJwtException(msg);
}
//如果有第三段,设置数字签名部分的base64的字符串
if (sb.length() > 0) {
base64UrlEncodedDigest = sb.toString();
}
//如果payload的字符串为空则表示payload的数据被删除了,抛出异常
if (base64UrlEncodedPayload == null) {
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
}
// =============== Header =================
Header header = null;
//设置压缩方式,一般没有设置
CompressionCodec compressionCodec = null;
//解压得到JWT Header部分。
if (base64UrlEncodedHeader != null) {
String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
Map m = readValue(origValue);
if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}
compressionCodec = compressionCodecResolver.resolveCompressionCodec(header);
}
// =============== Body =================
//获取到payload部分
String payload;
if (compressionCodec != null) {
byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
payload = new String(decompressed, Strings.UTF_8);
} else {
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
}
Claims claims = null;
//将payload由json字符串转成一个hashMap
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
Map claimsMap = readValue(payload);
claims = new DefaultClaims(claimsMap);
}
// =============== Signature =================
//如果数字签名部位空
if (base64UrlEncodedDigest != null) { //it is signed - validate the signature
JwsHeader jwsHeader = (JwsHeader) header;
SignatureAlgorithm algorithm = null;
if (header != null) {
String alg = jwsHeader.getAlgorithm();
if (Strings.hasText(alg)) {
//获取到签名算法
algorithm = SignatureAlgorithm.forName(alg);
}
}
if (algorithm == null || algorithm == SignatureAlgorithm.NONE) {
//it is plaintext, but it has a signature. This is invalid:
String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " +
"algorithm.";
throw new MalformedJwtException(msg);
}
if (key != null && keyBytes != null) {
throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either.");
} else if ((key != null || keyBytes != null) && signingKeyResolver != null) {
String object = key != null ? "a key object" : "key bytes";
throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either.");
}
//digitally signed, let's assert the signature:
Key key = this.key;
//获取加密盐
if (key == null) { //fall back to keyBytes
byte[] keyBytes = this.keyBytes;
if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
if (claims != null) {
key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
} else {
key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
}
}
if (!Objects.isEmpty(keyBytes)) {
Assert.isTrue(algorithm.isHmac(),
"Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");
key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
}
}
Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");
//加密的数据内容
//re-create the jwt part without the signature. This is what needs to be signed for verification:
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;
JwtSignatureValidator validator;
try {
//通过加密算法给加密盐获取到JWT的数字签名验证器
validator = createSignatureValidator(algorithm, key);
} catch (IllegalArgumentException e) {
String algName = algorithm.getValue();
String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
"algorithm, but the specified signing key of type " + key.getClass().getName() +
" may not be used to validate " + algName + " signatures. Because the specified " +
"signing key reflects a specific and expected algorithm, and the JWT does not reflect " +
"this algorithm, it is likely that the JWT was not expected and therefore should not be " +
"trusted. Another possibility is that the parser was configured with the incorrect " +
"signing key, but this cannot be assumed for security reasons.";
throw new UnsupportedJwtException(msg, e);
}
//通过验证器来验证JWT是否有效
if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) {
String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " +
"asserted and should not be trusted.";
throw new SignatureException(msg);
}
}
final boolean allowSkew = this.allowedClockSkewMillis > 0;
//since 0.3:
if (claims != null) {
SimpleDateFormat sdf;
final Date now = this.clock.now();
long nowTime = now.getTime();
//https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4
//token MUST NOT be accepted on or after any specified exp time:
Date exp = claims.getExpiration();
//验证JWT是否过期
if (exp != null) {
long maxTime = nowTime - this.allowedClockSkewMillis;
Date max = allowSkew ? new Date(maxTime) : now;
if (max.after(exp)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String expVal = sdf.format(exp);
String nowVal = sdf.format(now);
long differenceMillis = maxTime - exp.getTime();
String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
differenceMillis + " milliseconds. Allowed clock skew: " +
this.allowedClockSkewMillis + " milliseconds.";
throw new ExpiredJwtException(header, claims, msg);
}
}
//https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5
//token MUST NOT be accepted before any specified nbf time:
Date nbf = claims.getNotBefore();
if (nbf != null) {
long minTime = nowTime + this.allowedClockSkewMillis;
Date min = allowSkew ? new Date(minTime) : now;
if (min.before(nbf)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String nbfVal = sdf.format(nbf);
String nowVal = sdf.format(now);
long differenceMillis = nbf.getTime() - minTime;
String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal +
", a difference of " +
differenceMillis + " milliseconds. Allowed clock skew: " +
this.allowedClockSkewMillis + " milliseconds.";
throw new PrematureJwtException(header, claims, msg);
}
}
validateExpectedClaims(header, claims);
}
Object body = claims != null ? claims : payload;
//返回JWT对象
if (base64UrlEncodedDigest != null) {
return new DefaultJws
下面我们来回答上面的3个问题作为一个JWT的总结内容。
这三段内容到底是什么呢?有什么作用呢?安全起见为什么一定是三段呢?
1 这三段内容分别是加密算法,数据信息体,以及数字签名
2 第一段的作用是表明算法,第二段是数据信息的传递,第三段是第一段和第二段加密之后的数据
3 因为在解析的时候防止别人拦截了JWT,防止篡改JWT中的数据,所以添加了第三段,需要通过第一段和第二段正确的信息进行验证是否正确。第三段的加密算法一般使用不可逆的加密算法比较好,安全系数比较高。