1 JWT概述

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。此信息可以通过数字签名被验证和信任。它是目前最流行的跨域身份验证解决方案。

1.1 JWT的应用场景

  • Authorization(授权):身份验证(前后端分离、微信小程序、app开发)和授权是目前使用最多场景,解决单点登录问题。JWT使用起来轻便、开销小,服务端不用记录用户状态信息。
  • Information Exchange (信息交换) :JWT可以被数字签名,通过它,可以安全的在各方之间传输信息。同时,签名是使用头和有效负载计算的,可以验证传递的信息内容有没有被篡改。

1.2 JWT的数据结构

java微信小程序获取用户昵称 小程序jwt获取用户信息_ci

JWT由三部分组成,分别是头信息、有效载荷、签名,它们之间用圆点(.)连接。

  • Header(头信息):由两部分组成,分别为令牌类型(typ)、散列算法(alg)
  • 它是一串使用Base64 URL算法将该头信息JSON对象转换的字符串。
  • 令牌类型统一为JWT。
  • 散列算法可以为HMAC、RSASSA、RSASSA-PSS等。
  • Payload(有效载荷):它包含claims。claims是关于实体(通常是用户)和其他数据的声明。claims有三种类型:registered、public、private。
  • 它是JWT的主体内容部分,同时它也是一个JSON对象,包含需要传递的数据。
  • registered:一组预定义的声明,推荐使用。
  • iss(issuer):发行人
  • exp(expiration time):到期时间
  • sub(subject):主题
  • aud(audience):观众
  • iat(Issued At):签发时间
  • jti(JWT ID):JWT ID(唯一标识)
  • nbf(Not Before):生效时间
  • public:自定义声明,不能与JWT注册表中属性冲突。
  • private:自定义声明,用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
  • 它也是一串使用Base64 URL算法加密的字符串。
  • signature(签名):是对前两部分的签名,用于验证消息在传递过程中是否被篡改。
  • 创建签名:编码的头信息、编码的有效载荷信息、密钥,使用头信息中的加密算法对三者进行加密。

1.3 JWT使用方式

客户端登录成功后收到服务器返回的JWT信息,可以存储在Cookie里面,也可以存储在localStorage。此后,客户端每次与服务器通信,都要带上这个JWT信息。一般为了解决跨域请求问题,通常的做法就是放在HTTP请求的头信息Authorization字段里面。服务器验证JWT信息的有效性来与客户端通信。

1.4 JWT特点

  • JWT默认是不加密,但也是可以加密的。生成原始token以后,可以用密钥再加密一次。
  • JWT不加密的情况下,不能将秘密数据写入JWT。
  • JWT不仅可以用于认证,也可以用于交换信息。有效使用JWT,可以降低服务器查询数据库的次数。
  • JWT的最大缺点是,由于服务器不保存session状态,因此在使用过程中无法中途废止某个token,或者更改token的权限。也就是说,一旦token签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置的比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • 为了减少盗用,JWT不应该使用HTTP协议明码传输,要使用HTTPS协议传输。

2 JWT使用

2.1 搭建JWT使用环境

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.5</version>
    <scope>runtime</scope>
</dependency>

2.2 JwtHelper工具类

package com.chtwm.component.constant;

/**
 * @Description: JWT使用常量值
 */
public class SecretConstant {

    //签名秘钥
    public static final String BASE64SECRET = "ZW]4l5JH[m6Lm)LaQEjpb!4E0lRaG(";

    //超时毫秒数(默认30分钟)
    public static final int EXPIRESSECOND = 1800000;

    //用于JWT加密的密匙
    public static final String DATAKEY = "u^3y6SPER41jm*fn";

}
package com.chtwm.component.secret;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class AESSecretUtil {

    /**秘钥的大小*/
    private static final int KEYSIZE = 128;
    
    /**
     * @Description: AES加密
     * @param data - 待加密内容
     * @param key - 加密秘钥
     */
    public static byte[] encrypt(String data, String key) {
        if(StringUtils.isNotBlank(data)){
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                //选择一种固定算法,为了避免不同java实现的不同算法,生成不同的密钥,而导致解密失败
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(key.getBytes());
                keyGenerator.init(KEYSIZE, random);
                SecretKey secretKey = keyGenerator.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                Cipher cipher = Cipher.getInstance("AES");// 创建密码器
                byte[] byteContent = data.getBytes("utf-8");
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);// 初始化
                byte[] result = cipher.doFinal(byteContent);
                return result; // 加密
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * @Description: AES加密,返回String
     * @param data - 待加密内容
     * @param key - 加密秘钥
     */
    public static String encryptToStr(String data, String key){
        return StringUtils.isNotBlank(data)?parseByte2HexStr(encrypt(data, key)):null;
    }


    /**
     * @Description: AES解密
     * @param data - 待解密字节数组
     * @param key - 秘钥
     */
    public static byte[] decrypt(byte[] data, String key) {
        if (ArrayUtils.isNotEmpty(data)) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                //选择一种固定算法,为了避免不同java实现的不同算法,生成不同的密钥,而导致解密失败
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(key.getBytes());
                keyGenerator.init(KEYSIZE, random);
                SecretKey secretKey = keyGenerator.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                Cipher cipher = Cipher.getInstance("AES");// 创建密码器
                cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);// 初始化
                byte[] result = cipher.doFinal(data);
                return result; // 加密
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * @Description: AES解密,返回String
     * @param enCryptdata - 待解密字节数组
     * @param key - 秘钥
     */
    public static String decryptToStr(String enCryptdata, String key) {
        return StringUtils.isNotBlank(enCryptdata)?new String(decrypt(parseHexStr2Byte(enCryptdata), key)):null;
    }

    /**
     * @Description: 将二进制转换成16进制
     * @param buf - 二进制数组
     */
    public static String parseByte2HexStr(byte buf[]) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < buf.length; i++) {
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * @Description: 将16进制转换为二进制
     * @param hexStr - 16进制字符串
     */
    public static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length()/2];
        for (int i = 0;i< hexStr.length()/2; i++) {
            int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
            int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }

}
package com.chtwm.component.secret;


/**
 * @Description: Base64转码解码工具类
 */
public class Base64Util {

	//字母表
	private static char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toCharArray();

	private static byte[] codes = new byte[256];

	static {
		for (int i = 0; i < 256; i++) {
			codes[i] = -1;
		}
		for (int i = 'A'; i <= 'Z'; i++) {
			codes[i] = (byte) (i - 'A');
		}

		for (int i = 'a'; i <= 'z'; i++) {
			codes[i] = (byte) (26 + i - 'a');
		}
		for (int i = '0'; i <= '9'; i++) {
			codes[i] = (byte) (52 + i - '0');
		}
		codes['+'] = 62;
		codes['/'] = 63;
	}

		/**
		 * @Description: 功能:编码字符串
		 * @param data
		 */
	    public static String encode(String data) {
	        return new String(encode(data.getBytes()));
	    }

	    /**
	     * @Description: 功能:解码字符串
	     * @param data
	     */
	    public static String decode(String data) {
	        return new String(decode(data.toCharArray()));
	    }

	    /**
	     * @Description: 功能:编码byte[]
	     * @param data - 字节数组
	     */
	    public static char[] encode(byte[] data) {
	        char[] out = new char[((data.length + 2) / 3) * 4];
	        for (int i = 0, index = 0; i < data.length; i += 3, index += 4) {
	            boolean quad = false;
	            boolean trip = false;

	            int val = (0xFF & (int) data[i]);
	            val <<= 8;
	            if ((i + 1) < data.length) {
	                val |= (0xFF & (int) data[i + 1]);
	                trip = true;
	            }
	            val <<= 8;
	            if ((i + 2) < data.length) {
	                val |= (0xFF & (int) data[i + 2]);
	                quad = true;
	            }
	            out[index + 3] = alphabet[(quad ? (val & 0x3F) : 64)];
	            val >>= 6;
	            out[index + 2] = alphabet[(trip ? (val & 0x3F) : 64)];
	            val >>= 6;
	            out[index + 1] = alphabet[val & 0x3F];
	            val >>= 6;
	            out[index + 0] = alphabet[val & 0x3F];
	        }
	        return out;
	    }

	    /**
	     * @Description: 功能:解码字节数组
	     * @param data - 字节数组
	     */
	    public static byte[] decode(char[] data) {

	        int tempLen = data.length;
	        for (int ix = 0; ix < data.length; ix++) {
	            if ((data[ix] > 255) || codes[data[ix]] < 0) {
	                --tempLen; // ignore non-valid chars and padding
	            }
	        }

	        int len = (tempLen / 4) * 3;
	        if ((tempLen % 4) == 3) {
	            len += 2;
	        }
	        if ((tempLen % 4) == 2) {
	            len += 1;

	        }
	        byte[] out = new byte[len];

	        int shift = 0; 
	        int accum = 0; 
	        int index = 0;

	        for (int ix = 0; ix < data.length; ix++) {
	            int value = (data[ix] > 255) ? -1 : codes[data[ix]];

	            if (value >= 0) { 
	                accum <<= 6; 
	                shift += 6; 
	                accum |= value; 
	                if (shift >= 8) { 
	                    shift -= 8; 
	                    out[index++] = 
	                    (byte) ((accum >> shift) & 0xff);
	                }
	            }
	        }

	        if (index != out.length) {
	            throw new Error("Miscalculated data length (wrote " + index
	                    + " instead of " + out.length + ")");
	        }

	        return out;
	    }

}
package com.chtwm.component.util;

import com.alibaba.fastjson.JSONObject;
import com.chtwm.component.constant.SecretConstant;
import com.chtwm.component.secret.AESSecretUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description: JWT工具类
 * JWT的数据结构为:A.B.C三部分数据,由字符点"."分割成三部分数据
 * A-header头信息
 * B-payload 有效负荷 一般包括:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)
 * C-signature 签名信息 是将header和payload进行加密生成的
 */
public class JwtHelper {

    private static Logger logger = LoggerFactory.getLogger(JwtHelper.class);

    /**
     * @Description: 生成JWT字符串
     * 格式:A.B.C
     * A-header头信息
     * B-payload 有效负荷
     * C-signature 签名信息 是将header和payload进行加密生成的
     * @param userId - 用户编号
     * @param userName - 用户名
     * @param identities - 客户端信息(变长参数),目前包含浏览器信息,用于客户端拦截器校验,防止跨域非法访问
     */
    public static String generateJWT(String userId, String userName, String ...identities) {
        //签名算法,选择SHA-256
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //获取当前系统时间
        long nowTimeMillis = System.currentTimeMillis();
        Date now = new Date(nowTimeMillis);
        //将BASE64SECRET常量字符串使用base64解码成字节数组
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);
        //使用HmacSHA256签名算法生成一个HS256的签名秘钥Key
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        //添加构成JWT的参数
        Map<String, Object> headMap = new HashMap<>();
        /*
            Header
            {
              "alg": "HS256",
              "typ": "JWT"
            }
         */
        headMap.put("alg", SignatureAlgorithm.HS256.getValue());
        headMap.put("typ", "JWT");
        JwtBuilder builder = Jwts.builder().setHeader(headMap)
                /*
                    Payload
                    {
                      "userId": "1234567890",
                      "userName": "John Doe",
                    }
                 */
                //加密后的客户编号
                .claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))
                //客户名称
                .claim("userName", userName)
                //客户端浏览器信息
                .claim("userAgent", identities[0])
                //Signature
                .signWith(signatureAlgorithm, signingKey);
        //添加Token过期时间
        if (SecretConstant.EXPIRESSECOND >= 0) {
            long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate).setNotBefore(now);
        }
        return builder.compact();
    }

    /**
     * @Description: 解析JWT
     * 返回Claims对象
     * @param jsonWebToken - JWT
     */
    public static Claims parseJWT(String jsonWebToken) {
        Claims claims = null;
        try {
            if (StringUtils.isNotBlank(jsonWebToken)) {
                //解析jwt
                claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))
                        .parseClaimsJws(jsonWebToken).getBody();
            }else {
                logger.warn("[JWTHelper]-json web token 为空");
            }
        } catch (Exception e) {
            logger.error("[JWTHelper]-JWT解析异常:可能因为token已经超时或非法token");
        }
        return claims;
    }

    /**
     * @Description: 校验JWT是否有效
     * 返回json字符串的demo:
     * {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"}
     * freshToken-刷新后的jwt
     * userName-客户名称
     * userId-客户编号
     * userAgent-客户端浏览器信息
     * @param jsonWebToken - JWT
     */
    public static String validateLogin(String jsonWebToken) {
        Map<String, Object> retMap = null;
        Claims claims = parseJWT(jsonWebToken);
        if (claims != null) {
            //解密客户编号
            String decryptUserId = AESSecretUtil.decryptToStr((String)claims.get("userId"), SecretConstant.DATAKEY);
            retMap = new HashMap<>();
            //加密后的客户编号
            retMap.put("userId", decryptUserId);
            //客户名称
            retMap.put("userName", claims.get("userName"));
            //客户端浏览器信息
            retMap.put("userAgent", claims.get("userAgent"));
            //刷新JWT
            retMap.put("freshToken", generateJWT(decryptUserId, (String)claims.get("userName"), (String)claims.get("userAgent"), (String)claims.get("domainName")));
        }else {
            logger.warn("[JWTHelper]-JWT解析出claims为空");
        }
        return retMap!=null?JSONObject.toJSONString(retMap):null;
    }
}