1 JWT概述
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。此信息可以通过数字签名被验证和信任。它是目前最流行的跨域身份验证解决方案。
1.1 JWT的应用场景
- Authorization(授权):身份验证(前后端分离、微信小程序、app开发)和授权是目前使用最多场景,解决单点登录问题。JWT使用起来轻便、开销小,服务端不用记录用户状态信息。
- Information Exchange (信息交换) :JWT可以被数字签名,通过它,可以安全的在各方之间传输信息。同时,签名是使用头和有效负载计算的,可以验证传递的信息内容有没有被篡改。
1.2 JWT的数据结构
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;
}
}