Advertisement

JWT(JSON WEB TOKEN)

阅读量:

1 什么是JWT

JWT是基于json制作的一个web token的一套规范,这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息,它属于一种无状态的数据,它主要有两大使用场景:认证和数据传递。

2 JWT 的内容

JWT的内容如下:

eyJhbGciOiJIUzUxMiJ9.eyJwYXNzd29yZCI6IjQ1NiIsImV4cCI6MTY2MDUyOTI0MCwiaWF0IjoxNjYwNTI3NDQwLCJ1c2VybmFtZSI6InpoYW5nc2FuIn0.LbtbH4DRn7W8PAzre1Cflpfg3W9grzPQIs7Z_86q5J9Gm8YiRxvBjZ7gPmils6LBzvmFKmvcgywwHELLPlCQKQ

由上面的JWT的内容我们根据“.”这个字符把内容隔开看,内容一共分为三段,如下
eyJhbGciOiJIUzUxMiJ9
eyJwYXNzd29yZCI6IjQ1NiIsImV4cCI6MTY2MDUyOTI0MCwiaWF0IjoxNjYwNTI3NDQwLCJ1c2VybmFtZSI6InpoYW5nc2FuIn0
LbtbH4DRn7W8PAzre1Cflpfg3W9grzPQIs7Z_86q5J9Gm8YiRxvBjZ7gPmils6LBzvmFKmvcgywwHELLPlCQKQ
这三段内容也就是jwt的主要组成部分,看到这三段内容,我们肯定会不由自主的想到,这三段内容到底是什么呢?有什么作用呢?安全起见为什么一定是三段呢?

2.1 第一段名为Header

header里面存放的是一个内容包含了算法名称已经类型的json对象,然后以base64处理之后的数据。

2.2 第二段名为Payload

payLoad 里面存放的是user的数据的json对象,然后以base64处理之后的数据。

2.3 第三段名为 Signature

signature 里面存放的是前面两个的数据组合起来,然后加上一个加密盐通过第一段设置的加密方式加密起来,然后以base64处理的数据。

3 JWT的demo

在 pom.xml导入依赖 如下

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<String, Object> 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<String,Object> parseToken(String token) {
    
 		
    
 		Claims claims = Jwts.parser()
    
 				//设置加密盐
    
 				.setSigningKey(keyScret)
    
 				//通过压缩的jwt 
    
 				//字符串解析出数据信息
    
 				.parseClaimsJws(token)
    
 				//返回数据信息;
    
 				//返回的内容是一个hashMap
    
 				.getBody();
    
 		return claims;
    
 	}
    
 	
    
 	public static void main(String[] args) {
    
 		
    
 		Map<String, Object> map = new HashMap<String,Object>(); JwtUtil jwtUtil = new
    
 		JwtUtil(); map.put("username", "zhangsan"); map.put("password", "456");
    
 		 
    
 		String token =jwtUtil.getToken(map); System.out.println(token);
    
 		Map<String,Object> map2 = jwtUtil.parseToken(token); Set<String> 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也是通过“.”隔开的三段字符串拼接而成。接下来我没还是带着我之前提出的三个问题,分析下源码。

4 JWT的源码分析

4.1 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;
    
     }

4.2 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<String, Object> 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<String, Object> 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<Object>((JwsHeader) header, body, base64UrlEncodedDigest);
    
     } else {
    
         return new DefaultJwt<Object>(header, body);
    
     }
    
     }

5 总结

下面我们来回答上面的3个问题作为一个JWT的总结内容。

这三段内容到底是什么呢?有什么作用呢?安全起见为什么一定是三段呢?

1 这三段内容分别是加密算法,数据信息体,以及数字签名

2 第一段的作用是表明算法,第二段是数据信息的传递,第三段是第一段和第二段加密之后的数据

3 因为在解析的时候防止别人拦截了JWT,防止篡改JWT中的数据,所以添加了第三段,需要通过第一段和第二段正确的信息进行验证是否正确。第三段的加密算法一般使用不可逆的加密算法比较好,安全系数比较高。

全部评论 (0)

还没有任何评论哟~