浅析JWT

随着移动互联网的兴起,传统基于session/cookie的web网站认证方式转变为了基于OAuth2等开放授权协议的单点登录模式(SSO),相应的基于服务器session+浏览器cookie的Auth手段也发生了转变,Json Web Token的出现成为了当前的热门的Token Auth机制。

Cookie和Session相关概念请见博文:Cookie-Session

Json Web Token(JWT)

Json web token (JWT) 是目前最流行的跨域认证解决方案,JWT是一个开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,可以在客户端与服务器之间作为JSON对象安全地传输信息。

官方定义:JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties

现在网上大多数介绍JWT的文章实际介绍的都是JWS(JSON Web Signature),这也往往导致了大家对于JWT的误解,但是JWT并不等于JWS,JWS只是JWT的一种实现,除了JWS外,JWE(JSON Web Encryption)也是JWT的一种实现。

下面就来详细介绍一下JWS与JWE的两种实现方式:

JSON Web Signature(JWS)

JSON Web Signature 是一个有着简单的统一表达形式的字符串。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如:Token类型以及签名所用的算法等。

JSON内容要经Base64 编码生成字符串才能成为Header。

示例

1
2
3
4
{
"typ":"jwt"
"alg":"HS256" //加密算法
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

载荷(PayLoad)

payload的五个字段都是由JWT的标准所定义的。

  1. iss: 该JWT的签发者;
  2. sub: 该JWT所面向的用户
  3. aud: 接收该JWT的一方;
  4. exp(expires): 什么时候过期,这里是一个Unix时间戳
  5. iat(issued at): 在什么时候签发的,即JWT生成的时间;

还有一些其他的信息没有列举出来,也都是可以按需补充的。

JSON内容要经Base64 编码生成字符串成为PayLoad。

示例

1
2
3
{
"name":"Maple"
}

对其进行base64加密,得到JWT的第二部分。

1
eyJqdGkiOiIyOGYxNmE4YS1mMDM1LTRkYWQtYTFiNy01N2YwNzdmM2ZkZjUiLCJzdWIiOiJUZXN0IiwiaWF0IjoxNjgzMjcxOTE0LCJleHAiOjE2ODMyNzE5NzQsImF1ZCI6Ild1emkiLCJuYW1lIjoiTWFwbGUifQ

签名(Signature)

这个部分是由base64加密后的header和base64加密后的payload,使用.连接组成的字符串,然后通过header中声明的加密方式,并使用密钥secret进行加密,生成签名。

示例

由上述base64加密的Header和PayLoad拼接并加密得到签名部分:

1
Lu1NgVnhdlOYFBrki6HPx15X6hl8tnGB4jGWRdondNQ

JWS的主要目的是保证了数据在传输过程中不被修改,验证数据的完整性。

上述示例得到的完整的JWT整体格式为 header.payload.signature ,具体值为:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyOGYxNmE4YS1mMDM1LTRkYWQtYTFiNy01N2YwNzdmM2ZkZjUiLCJzdWIiOiJUZXN0IiwiaWF0IjoxNjgzMjcxOTE0LCJleHAiOjE2ODMyNzE5NzQsImF1ZCI6Ild1emkiLCJuYW1lIjoiTWFwbGUifQ.Lu1NgVnhdlOYFBrki6HPx15X6hl8tnGB4jGWRdondNQ

JSON Web Encryption(JWE)

相对于JWS,JWE则同时保证了安全性与数据完整性。JWE由五部分组成:

具体生成步骤为:

  1. JOSE含义与JWS头部相同。
  2. 生成一个随机的Content Encryption Key (CEK)。
  3. 使用RSAES-OAEP 加密算法,用公钥加密CEK,生成JWE Encrypted Key。
  4. 生成JWE初始化向量。
  5. 使用AES GCM加密算法对明文部分进行加密生成密文Ciphertext,算法会随之生成一个128位的认证标记Authentication Tag。
  6. 对五个部分分别进行base64编码。

可见,JWE的计算过程相对繁琐,不够轻量级,因此适合数据传输而非token认证,但该协议也足够安全可靠,用简短字符串描述了传输内容,兼顾数据的安全性与完整性。

JWT的使用

一般实际开发中说的JWT,实际上都是JWS,所以下述JWT默认就是JWS。

什么是JWT?

json web token 通过数字签名的方法,以json对象为载体,在不同的服务器之间安全的传输信息。
简单来说:把信息进行安全的封装,以json的形式进行安全的网络传输。

JWT的作用?

JWT最常见的场景就是授权认证,一旦用户进行了登录操作,服务端会返回一个JWT(token)给客户端,后续客户端的每个请求都在请求头中带上JWT(token),系统在每次处理用户请求之前,都要先进行JWT安全校验,通过之后在进行处理。

用户在提交登录信息后,服务器校验数据后将通过密文的方式来生成一个字符串token返回给客户端,客户端在之后的请求会把token放在header里,在请求到达服务器后,服务器会检验和解密token,如果token被篡改或者失效将会拒绝请求,如果有效则服务器可以获得用户的相关信息并执行请求内容,最后将结果返回。

SpringCloud下如何使用JWT?

在微服务架构下,通常有单独一个服务Auth去管理相关认证,为了安全不会直接让用户访问某个服务,会开放一个入口服务作为网关gateway,所有请求首先访问gateway,由gateway将请求路由到各个服务,通常的做法在网关里进行请求拦截校验,来保证项目的安全性,下图是JWT在微服务中流程图(图中采用非对称加密算法,利用私钥在auth加密,公钥在网关gateway中解密,由此来减轻auth压力,此模型设计并不一定通用,架构设计主要根据实际场景和领域模型划分,可灵活设计并运用JWT)。

针对非对称加密严格地说:公钥加密,私钥解密;私钥加签,公钥验签。

关于JWT有效期与安全性

JWT如果使用不当,服务器如同裸奔~~~,在使用JWT是一定要注意。

假如黑客监控电脑,抓包获取到JWT,伪造HTTP请求,对服务器是非常不安全的,常见的问题如下:

  • 黑客修改HTTP中body的信息进行操作 - 篡改
    • 修改body之后签名信息就不正确,然后就无法验证签名,说明数据被修改,数字签名的意义所在;
  • 黑客伪造用户JWT进行访问和操作 - 伪造;
    • 无法使用服务器的签名,所以在保证密钥不被泄露的情况下,不会被渗透;如果签名算法和秘钥泄露,那就是裸奔了;
  • 黑客窃取JWT,模仿真实用户进行操作 - 冒充;
    • 解决办法:
      • 对敏感api接口,采用https,https是在http超文本传输协议加入SSL层,它在网络间通信是加密的,所以需要加密证书。
      • 或者在代码层面进行优化做安全检测:比如根据ip地址,设备码,一次性token机制,token时效期等措施来解决项目安全性问题等等

JWT工具类

引入依赖

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

代码

此处只是一个Demo,在实际的开发中,还可以进行更个性化的封装,尤其是针对payload荷载部分,一般在开发中需要保存在JWT中的用户信息是一个POJO对象,而原生JWTBuilder中针对荷载部分的设置是使用 claim() 方法,用起来不是很方便,可以进行一个防腐层封装。

防腐层封装的个人思考

JWT工具类的作用范围定位是全局,甚至是可以将其封装为一个基础的二方包,所以防腐层必须具有通用性,那么就不能使用POJO类型,因为无法确定需要支持的POJO的具体类型,可以考虑使用 Properties类型 、String类型、JSON串等。

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import io.jsonwebtoken.*;
import io.jsonwebtoken.lang.Collections;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
* JWT工具类
*
* @author Maple
* @date 2023/5/5
*/
public class JwtUtils {

private static final String KEY_PREFIX = "Maple";

/**
* 生成JWT
* 其中可选的参数是一些辅助性参数,在使用方可以用于一些校验.
*
* @param issuer 签发者|可选
* @param subject JWT所面向的用户主体|可选
* @param audience 接收JWT的一方|可选
* @param key 自定义秘钥
* @param ttlMillis 过期时间|可选
* @param claimMap 载荷集合,就是需要加密的用户信息
* @return
* @throws Exception
*/
public static String createJWT(String issuer, String subject, String audience, String key, long ttlMillis, HashMap<String, Object> claimMap) throws Exception {

SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);

// 封装自定义秘钥
SecretKey secretKey = generateKey(key);

// 使用JWT自带的构造器构造一个jwt
JwtBuilder builder = Jwts.builder()
// 封装header属性
.setHeaderParam("alg", "AES") // 加密算法类型
.setHeaderParam("typ", "JWT") // token类型
// 封装payload属
.setId(UUID.randomUUID().toString()) // JWT id,唯一标识
// .setIssuer(issuer) // 签发者
// .setSubject(subject) // 面向的用户主体
.setIssuedAt(new Date()) // JWT创建时间
.setExpiration(now) // JWT过期时间
.setAudience(audience) // 接收JWT的一方
// 构造signature部分
.signWith(signatureAlgorithm, secretKey);

// 封装payload里的信息 使用claim方法
if (!Collections.isEmpty(claimMap)) {
for (Map.Entry<String, Object> entry : claimMap.entrySet()) {
builder.claim(entry.getKey(), entry.getValue());
}
}

// 设置JWT的过期时间
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}

return builder.compact();
}

/**
* 解密JWT
*
* @param key 自定义秘钥
* @param jwt 待解析的JWT
* @return Clamins, 即payload部分.
* @throws Exception
*/
public static Claims parseJWTClaims(String key, String jwt) throws Exception {
Jws<Claims> claimsJws = parseJWT(key, jwt);
return claimsJws.getBody();
}

/**
* 解析JWT
*
* @param key 自定义秘钥
* @param jwt 待解析的JWT
* @return Jws<Claims>,包含Header,Clamins,Signature三个部分.
* @throws Exception
*/
public static Jws<Claims> parseJWT(String key, String jwt) throws Exception {

SecretKey secretKey = generateKey(key);
// 通过这个签名key对token进行解析
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt);
return claimsJws;

}

/**
* 自定义字符串生成加密的秘钥|用于后续HS256加密加签.
*
* @param key
* @return
*/
private static SecretKey generateKey(String key) {
String stringKey = KEY_PREFIX + key;
// base64 两种写法
byte[] keySecretBytes = Base64.decodeBase64(stringKey);
// byte[] keySecretBytes = DatatypeConverter.parseBase64Binary(stringKey);
SecretKey secretKey = new SecretKeySpec(keySecretBytes, 0, keySecretBytes.length, SignatureAlgorithm.HS256.toString());
return secretKey;
}

}

测试:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
HashMap<String, Object> map = new HashMap<>();
map.put("name", "Maple");
String jwt = createJWT("Maple", "Test", "Wuzi", "fjeoahgoeja", 60 * 1000L, map);
System.out.println("jwt = " + jwt);
Jws<Claims> claimsJws = parseJWT("fjeoahgoeja", jwt);
System.out.println(claimsJws.getHeader().getAlgorithm());
System.out.println(claimsJws.getBody().getExpiration());
}

输出:

1
2
3
jwt = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyOGYxNmE4YS1mMDM1LTRkYWQtYTFiNy01N2YwNzdmM2ZkZjUiLCJzdWIiOiJUZXN0IiwiaWF0IjoxNjgzMjcxOTE0LCJleHAiOjE2ODMyNzE5NzQsImF1ZCI6Ild1emkiLCJuYW1lIjoiTWFwbGUifQ.Lu1NgVnhdlOYFBrki6HPx15X6hl8tnGB4jGWRdondNQ
HS256
Fri May 05 15:32:54 CST 2023

JWT的生成特点:可以重复请求,每次请求都会生成一个新的JWT,旧JWT依旧有效。


参考文档:

一篇文章带你分清楚JWT,JWS与JWE

JWT技术简介和使用

JWT在微服务系统中的如何应用,如何保证安全性?