JWT简介及使用

前言

JWT(JSON Web Tokens)是目前比较流行的跨域认证解决方案,遵循RFC 7519 标准,我们可以使用JWT在用户和服务器之间传递安全可靠的信息。

我们可以在 JWT的官网 了解到更多关于JWT的信息。

正文

构成

JWT主要由头部载荷签名三部分构成,三部分的信息用英文逗号“.”分割。

一个完整的JWT如下:

upload successful

它的头部部分(Header)为红色标注部分,载荷(Payload)为蓝色部分,签名(Signature)为橘色部分。

它们均使用Base64进行编码,我们将上述Base64解码后可以看到如下:

Header部分Json:

1
2
3
4
{
"typ": "JWT",
"alg": "HS256"
}

这儿Header声明了使用的加密算法(HS256)及token类型(JWT)。

Payload部分Json如下:

1
2
3
4
5
6
7
{
"phone": "1888888888",
"sessionId": "111111111111",
"exp": 1548052800,
"userId": "1433223",
"platform": "APP"
}

其中除exp字段其它都是我自定义的字段。JWT 规定了7个官方字段,供我们选用,如下:

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

PS:可以看到信息仅仅使用了Base64进行了一下编码,非加密,如果我们想是信息更安全可以对JWT生成的token进行可逆加密。

Signature部分:

会对上面两部分进行签名,通常使用RSA,Hmac或者ECDSA等签名方式。用于防止上面两部分的数据遭到篡改。

源码

要想在Java项目里使用JWT,需要引入以下依赖。

1
2
3
4
5
6
<!--JSON Web Tokens-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>

我们来简单的看一下它的源码部分。

先看一下包的结构:

algorithm:各种签名的包。
exceptions:自定义异常类的包。
interfaces和impl:JWT的接口和实现类包。

upload successful

在algorithm包里我们可以看到我们刚才描述的几种签名算法(RSA,HMAC,ECDSA)。

在JWTCreator类中,我们可以看到Signature部分是通过Header,Payload经过签名算法得来的。

upload successful

同时载荷Payload里JWT规定的几个可使用字段也能看到。

upload successful

生成签名,放入Header信息及Payload信息,使用指定签名算法生成JWT token。

upload successful

JWTDecoder类为解密token的类,可以看到它的处理方法,获取headerJson和payloadJson,还是比较好理解的。

upload successful

应用

我们使用JWT生成token并使用。

我们知道,前后端使用token进行交互,服务器端可以不用保存session状态,减轻压力。

我们定义一个Vo,用于存放一些用户数据,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
@Accessors(chain = true)
public class UserVo {
/**
* 用户手机号
*/
private String phone;
/**
* 用户唯一Id
*/
private String userId;
/**
* sessionId
*/
private String sessionId;
/**
* 过期时间
*/
private long expiresAt;
/**
* 用户所属平台
*/
private String platform;
}

可以认为这些为公共部分,用户登录后应该携带这些信息。

这样我们就可以使用JWT在该用户登录后生成一个有效token,为保证信息安全,我们可以对生成的token进行加密,如下:

我们使用AES算法对token进行加解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@Slf4j
public class AESUtils {
private final static String ENCODING_UTF8 = "utf-8";
private static final String KEY_ALGORITHM = "AES";
/**默认的加密算法*/
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private String secretKeySeed = null;
private String ivParameterSeed = null;
public AESUtils(String secretKeySeed,String ivParameterSeed) {
this.secretKeySeed = secretKeySeed;
if (ivParameterSeed.length() != 16) {
throw new RuntimeException("iv向量长度必须为16");
}
this.ivParameterSeed=ivParameterSeed;
}
public AESUtils(String secretKeySeed) {
if (secretKeySeed.length() != 16) {
throw new RuntimeException("iv向量长度必须为16");
}
this.secretKeySeed = secretKeySeed;
this.ivParameterSeed=secretKeySeed;
}
/**
* AES加密
* @param content
* @return String
*/
public String aesEncrypt(String content) throws Exception {
// AES加密
byte[] encryptStr = encrypt(content, secretKeySeed,ivParameterSeed);
// BASE64位加密
return Base64.encodeBase64String(encryptStr);
}
/**
* AES解密
* @param encryptStr
* @return String
*/
public String aesDecrypt(String encryptStr) throws Exception {
// BASE64位解密
byte[] decodeBase64 = Base64.decodeBase64(encryptStr);
// AES解密
return new String(decrypt(decodeBase64, secretKeySeed,ivParameterSeed),ENCODING_UTF8);
}

/**
* 生成加密秘钥
* @return
*/
private static SecretKeySpec getSecretKeySpec(final String secretKeySeed) {
try {
return new SecretKeySpec(secretKeySeed.getBytes(), "AES");
} catch (Exception ex) {
log.error("生成加密密钥异常",ex);
}
return null;
}

/**
* 生成向量秘钥
* @return
*/
private static IvParameterSpec getIvParameterSpec(final String ivParameterSeed) {
//使用CBC模式,需要一个向量iv,可增加加密算法的强度
return new IvParameterSpec(ivParameterSeed.getBytes());
}

/**
* 加密
* @param content
* @return byte[]
*/
private static byte[] encrypt(String content, String secretKeySeed,String ivParameterSeed) throws Exception {
// 创建密码器
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
// 初始化为加密模式的密码器
cipher.init(Cipher.ENCRYPT_MODE, getSecretKeySpec(secretKeySeed),getIvParameterSpec(ivParameterSeed));
// 加密
return cipher.doFinal(content.getBytes(ENCODING_UTF8));
}
/**
* 解密
* @param content
* @return byte[]
*/
private static byte[] decrypt(byte[] content, String secretKeySeed,String ivParameterSeed) throws Exception {
// 创建密码器
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
// 初始化为加密模式的密码器
cipher.init(Cipher.DECRYPT_MODE, getSecretKeySpec(secretKeySeed),getIvParameterSpec(ivParameterSeed));
// 解密
return cipher.doFinal(content);
}
}

同时,根据刚才我们的说明创建一个JWT帮助类用于生成token,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
@Slf4j
public class JWTUtils {
//JWT header
private String JWT_HEADER = "";
//签名算法
private Algorithm algorithm;
//默认token过期时间
private long expireTimeMillis=2*60*60*1000;
private AtomicBoolean initState = new AtomicBoolean(false);

private static AESUtils aesUtils=null;

private static class SingletonHolder {
static final JWTUtils instance = new JWTUtils();
}

public static JWTUtils getInstance() {
return JWTUtils.SingletonHolder.instance;
}

/**
* 初始化jwt
* @param jwtSecretKey
* @param jwtExpireTimeSeconds
* @param aesSecretKeySeed
* @param aesIvParameterSeed
* @throws UnsupportedEncodingException
*/
public void init(String jwtSecretKey,long jwtExpireTimeSeconds,String aesSecretKeySeed,String aesIvParameterSeed) throws UnsupportedEncodingException {
if (initState.compareAndSet(false, true)) {
this.algorithm = Algorithm.HMAC256(jwtSecretKey);
if(jwtExpireTimeSeconds>0) {
this.expireTimeMillis = jwtExpireTimeSeconds * 1000;
}
this.JWT_HEADER = StringUtils.substringBefore(JWT.create().sign(Algorithm.HMAC256(jwtSecretKey)), ".")+".";
aesUtils = new AESUtils(aesSecretKeySeed, aesIvParameterSeed);
}else{
log.error("重复初始化jwt");
}
}

/**
* 使用jwt生成token
* @param userVo
* @return
*/
public String encodeJWT(UserVo userVo) {
return encodeJWT(userVo,this.expireTimeMillis);
}

/**
* 生成token
* @param userVo
* @param expireTimeMillis
* @return
*/
public String encodeJWT(UserVo userVo,long expireTimeMillis){
String token = JWT.create()
.withExpiresAt(new Date(System.currentTimeMillis()+expireTimeMillis))
.withClaim("phone",userVo.getPhone())
.withClaim("userId",userVo.getUserId())
.withClaim("sessionId",userVo.getSessionId())
.withClaim("platform",userVo.getPlatform())
.sign(algorithm);
System.out.println("token----> "+token);
try {
token = aesUtils.aesEncrypt(StringUtils.removeStart(token, JWT_HEADER));
} catch (Exception ex) {
log.error("加密异常",ex);
token = "";
}
return token;
}

/**
* 解密token
* @param token
* @return
*/
public UserVo decodeJWT(String token) {
UserVo userVo =new UserVo();
try {
if (StringUtils.isBlank(token)) {
throw new RuntimeException("无效token");
}
String decryptJwtToken = aesUtils.aesDecrypt(token);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(StringUtils.join(JWT_HEADER,decryptJwtToken));
long expiresAt = jwt.getExpiresAt()==null?0:jwt.getExpiresAt().getTime();
if(System.currentTimeMillis()>expiresAt){
throw new RuntimeException("token有效期超期");
}
Map<String, Claim> claims = jwt.getClaims();
userVo.setPhone(claims.get("phone")==null?"":claims.get("phone").asString());
userVo.setUserId(claims.get("userId")==null?"":claims.get("userId").asString());
userVo.setSessionId(claims.get("sessionId")==null?"":claims.get("sessionId").asString());
userVo.setPlatform(claims.get("platform")==null?"":claims.get("platform").asString());
userVo.setExpiresAt(expiresAt);

} catch (Exception exception){
throw new RuntimeException("无效token");
}
return userVo;
}
}

这样,一个简单的JWT工具类就搞定了,可以用于生成token。

测试

我们测试一下效果,新建方法如下:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws Exception{
JWTUtils jwtUtils = JWTUtils.getInstance();
jwtUtils.init("sakuratears",1000,"1234567891111111","test111111111111");
UserVo userVo = new UserVo();
userVo.setUserId("1433223").setPhone("1888888888").setPlatform("APP").setSessionId("111111111111");
String token = jwtUtils.encodeJWT(userVo);
System.out.println("加密token----> "+token);
Thread.sleep(1);
UserVo vo = jwtUtils.decodeJWT(token);
System.out.println("解密token得到结果----> "+vo.toString());
}

运行结果如下:

upload successful

我们尝试缩短token失效时间,增加线程等待时间。如下:

1
2
3
jwtUtils.init("sakuratears",1,"1234567891111111","test111111111111");
......
Thread.sleep(10000);

再次运行,可以看到token已失效。

upload successful

所以我们在为客户端颁发token后,应该设置合理的token失效时间,当token失效后,再次请求,应告诉用户需要重新登录了。

总结

经过上面的一些描述,我们可以知道JWT的一些特点。

  1. JWT默认是不加密的,为了保证安全,可以对JWT(token)进行可逆加密处理。

  2. JWT可以用于信息交换,比如上面UserVo里面的手机号,这样我们不用在使用userId在对用户进行数据库查询,提高系统性能。

  3. 可以看到,JWT一旦签发生成token,如果不设置超时时间或者设置不合理(过长),在有效期内,token始终是有效的,除非服务器进行额外的处理。所以如果token泄露或被盗用,将是十分危险的,故应当设置合理的过期时间。

  4. 为了减少泄露或者盗用风险,JWT一般使用HTTPS协议进行传输。若使用HTTP协议,务必对token进行可逆加密处理后在进行传输。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道