一文读懂JWT工作机制

前言
之前开发互联网应用的时候一直使用 token 作为鉴权的手段,之前使用的时候仅仅当做类似 session 的存在,完全没有发挥 jwt-token 的价值。当遇到签发、授权、鉴权等流程不在同一主体时,就遇到了很多问题。查问题的时候又遇到一大堆名词,比如:JWT、JWE、JWA、JWS、JWK、JWKS,听起来就头皮发麻。为了完全理解它们,做一个整理。
这几个缩写对应的英文全称是:
JWT: JSON Web Token,令牌。JWE: JSON Web Encryption,加密。JWA: JSON Web Algorithm,算法。JWS: JSON Web Signature,签名。JWK: JSON Web Key,密钥。JWKS: JSON Web Key Set,多个密钥的集合。
JWT
JWT 是什么
先来看下 JWT,它其实是由两个点“.”分隔成三段的一串字符串:
# 头部说明.数据体.签名
header.payload.signature
这个字符串其实就是把数据用 base64 编码了一下,没有经过加密,任何人通过解码就能看到原始数据。
比如下面是一个真实的 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1Zjg5MGI4NTVjMGI3MDc1IiwicGljdHVyZSI6Imh0dHBzOi8vc3h5OTEuY29tL2F2YXRhci5wbmciLCJ1c2VybmFtZSI6IiIsImlkIjoiNWY4OTBiODU1MDM4YzBiNzA3NSIsImVtYWlsIjoic3h5OTFAbWUuY29tIiwic2lkIjoiNmUzYzIzZmJmNWQ1MDEzMmZlNTUiLCJhdWQiOiI1ZTQzYWIxNDFlODZhZGFmY2IiLCJleHAiOjE2MjUyMTc5MjAsImlhdCI6MTYyNTIxNDMyMCwiaXNzIjoiaHR0cHM6Ly91c2VyLnN4eTkxLmNvbS9vYXV0aC9vaWRjIn0.yDuGkG4JYTaDH15EHX7fB03BXMaSKbv1UUZlrxBMHAs
用 base64urlDecode 把第一个点前面的字符串解码,就可以可以知道 header 的内容:
header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
print(base64urlDecode(header))
// 输出
{"alg":"HS256","typ":"JWT"}
同理,把第二段解码后就得到数据
payload = 'eyJzdWIiOiI1Zjg5MGI4NTVjMGI3MDc1IiwicGljdHVyZSI6Imh0dHBzOi8vc3h5OTEuY29tL2F2YXRhci5wbmciLCJ1c2VybmFtZSI6IiIsImlkIjoiNWY4OTBiODU1MDM4YzBiNzA3NSIsImVtYWlsIjoic3h5OTFAbWUuY29tIiwic2lkIjoiNmUzYzIzZmJmNWQ1MDEzMmZlNTUiLCJhdWQiOiI1ZTQzYWIxNDFlODZhZGFmY2IiLCJleHAiOjE2MjUyMTc5MjAsImlhdCI6MTYyNTIxNDMyMCwiaXNzIjoiaHR0cHM6Ly91c2VyLnN4eTkxLmNvbS9vYXV0aC9vaWRjIn0';
print(base64urlDecode(payload))
// 输出
{"sub":"5f890b855c0b7075","picture":"https://sxy91.com/avatar.png","username":"","id":"5f890b855038c0b7075","email":"sxy91@me.com","sid":"6e3c23fbf5d50132fe55","aud":"5e43ab141e86adafcb","exp":1625217920,"iat":1625214320,"iss":"https://user.sxy91.com/oauth/oidc"}
第三段可以理解成随机字符串,客户端拿到也没什么用。
因此想要伪造这样格式的 JWT 也很简单:
data = base64urlEncode( header ) + "." + base64urlEncode( payload );
signature = "xxxx"; // 任意字符串
token = data + "." + signature
上面的过程仅仅为了说明 JWT 的生成过程和解析过程,更简单的方式是到jwt.io这个网站,它提供在线编辑或查看 JWT 格式的 token 。
所以 JWT 的作用是:
- 更方便的传输和转换数据结构 JSON 格式经过 base64Url 编码。
- 数据是没加密的,没有经过隐藏或者混淆数据。
- 使用 JWT 是为了保证发送的数据是由可信的来源创建的。
那么服务器如何判断这个 JWT 是自己合法发出的呢,关键点就在 signature ,只要保证别人无法知道 signature 怎么生成的就可以了。
最笨的办法是生成一个随机的字符串作为 signature 存在服务器端,然后发给客户端,以后收到 JWT 时取出 signature 与自己存储的做比对,一样就证明是正确的、合法的。
这样的问题是每个用户都需要存储一份数据,非常浪费资源。有没有一种方式不需要保存 signature 就能验证 JWT 是正确的呢?
简单的思路就是根据 header 和 payload 生成一个 signature,生成的规则只有服务器知道,这样就不用存储 signature,每次服务端收到客户端的 JWT 只需要重新生成一次 signature 与收到进行对比就知道是否合法。
比如可以设定这样的规则
data = base64urlEncode( header ) + "." + base64urlEncode( payload );
规则一:signature = data 的后32个字符。
规则二:signature = data 的第1,3,5,7...位字符串。
规则三:signature = data 的第1,3,5,7...位字符串。
规则四:signature = data 的第1,3,4,7,11,18...位字符串。
因为别人不知道我服务器用的是哪种规则,所以也就无法伪造了。
上面的规则看似别人不知道,其实想“猜”出来很简单,稍微聪明一点的人使用统计学和密码学相关知识,估计一周不到就能比较出来。
那有没有一种规则实现简单且理论上无法“猜”出来呢,这就需要用到哈希算法和非对称加密算法。
哈希算法
哈希算法,又称散列算法,它是一个单向函数,可以把任意长度的输入数据转化为固定长度的输出,此输出一般又称为摘要或者散列值,相当于人的指纹信息: 摘要=Hash(x)
这个算法有如下几个特点:
- 无论输入多长,输出总是一定的。
- 输出无规律,输入改变一丁点就会导致输出完全不同,因此无法统计和猜测。
- 通过输入可以很容易地计算输出,但是,反过来,通过输出无法反推输入。相当于警察抓到一个人就很容易得到他的指纹,但仅仅采集到一个指纹信息,却很难知道这个指纹是谁的,长相如何等。
例如,对洋葱和哈希算法这两个词进行某种哈希运算,得到的结果长度是一样的:
Hash("洋葱") = 74c9227d95c45856683c457bbcd04d90
Hash("哈希算法") = 16600f689cdf9ad8305749bd64d3ca32
Hash("哈希算术") = ef1f60e4a59184a4f5bb9977a2245218
Hash("哈希算力") = a47bc40c3586cc0fbf275d2f83c17708
Hash("哈希算了") = d67394d8dcda13ca451ae72e90ed2de2
能实现这种特性的算法有很多,常见的有:MD5、SHA、Hmac等。
哈希算法的这种特性就可以用来验证某个文件是否被篡改。早期很多提供软件的公司,除了提供软件的下载地址,同时也会提供文件的摘要。

比如你网速比较慢,不想去官网下载 MySQL,然后找同事通过 U 盘拷贝了一个 MySQL 的软件安装包,你想看一下这个软件是否安全,可以直接在终端执行命令md5 文件名获取这个安装包的 md5 值,然后和官网进行比对,如果一样就证明没修改过,不一眼就证明肯定不是官方发布的安装包。
回到 JWT ,使用哈希算法就可以保证数据的一致性,只要 header 和 payload 没有被改变,那么其生成的摘要也不会变。
token = header.payload.signature
摘要 = Hash(header+"."+payload);
到这里,如果用摘要作为 signature 那么似乎也没什么用,因为谁都能使用哈希算法轻易的生成这个摘要,伪造JWT比之前定义的规则还简单。
那能否加点干扰数据呢,比如:
token = header.payload.signature
摘要 = Hash(header+"."+payload);
signature = Hash(摘要+"这是我的密码,只有我知道")
这样也能保证别人无法伪造 signature ,这么做就需要密码设置的足够复杂,然而大多数人不会设置复杂的密码,即使看起来复杂的密码,对机器来说其实很简单。
完全使用 Hash 做 signature 也会遇到哈希碰撞的问题。60 位
那能不能由机器生成一个足够安全的密码,然后加密后作为 signature 呢?
非对称加密
JWT 由 JWK 经过一系列计算得来。
JWT其 header、payload 其实都是 json 数据,header 描述了 signature 使用了哪种算法,方便验证的时候选择响应的算法。
既然谁都可以方便的造出一个格式相符的 token ,那么服务器怎么验证这个 token 确实是服务器发出去的呢?,答案就是 signature 。
用 token 中第二个“.”前面的数据和服务器签发时的密码做一次哈西运算,如果结果与第三段的 signature 就证明是自己签发的。
Hash(header+"."+payload,secret) =? signature
这样的算法有什么用呢?把之前的 token 简化一下。
signature = Hash(data, secret);
token = data+"."+signature
服务器把数据和一个别人不知道的东西(密钥)进行一次哈希运算,得到一个独一无二的指纹(signature)。然后服务器把数据+指纹(token)发给前端。之后服务器收到 token(数据+指纹),只需要取出数据,通过计算密钥+数据的指纹与收到的指纹进行对比,就知道是否是自己发出的token,如果是就是合法的。也就做到了验证和防篡改。
这样的哈希算法有很多,有可能不同的 signature 使用的算法不同。那么验证的时候怎么知道要用哪个算法呢?
这就需要用到 JWK,其实就是用来存储密钥和哈希算法所需的各种参数。
{
"kid":"YoxRVsbyYE5zKzxAaiayKY9rVLl13xNbHIM_cDI18S4"
"kty":"RSA",
"alg":"RS256",
"use":"sig",
"e":"AQAB",
"n":"vL6fnf1S36B4xI3tIkD5_W3HoZJgEIzAYSsTLGIn",
}
密钥 ID (kid): 密钥类型 (kty):算法系列,RSA\EC\oct\OKP 算法 (alg):具体的算法, 使用使用 (use):enc、sig
RSA 指数 (e) RSA 模量 (n)

JWK 可以表示密钥,即可以表示公钥或私钥,也可同时表示公钥和私钥。提到公钥或私钥就不得不说非对称加密,这是一种特殊的算法。fx(私钥,message)=n, fy(公钥,n)=message要理解 JWT 就避不开哈希算法和非对称加密。
JOSE 规范
什么是 JOSE,它和 JWT 之间又有什么关系呢?
JOSE 全称 JSON Object Signing and Encryption ( RFC 7165[1] , RFC 7520[2] ),它定义了一系列的标准,用来规范网络传输过程中使用 JSON 的方式,上面说的 JWT 其实也是 JOSE 体系之一。

其中 JWT 又可分为 JWS 和 JWE 这两种不同的实现,我们大部分日常所使用的,所说的 JWT 其实应该属于 JWS 。
JWA 和 JWS 以及 JWK
JWA 的全称是 JSON Web Algorithms ( RFC 7518[3] ) ,字如其名, JOSE 体系中涉及到的所有算法就是它来定义的,比如通用算法有 Base64-URL 和 SHA,签名算法有 HMAC,RSA 和 Elliptic Curve(EC 椭圆曲线)。
我不会深入到算法原理,只是想让你知道 JWA 是做什么的。上述 JWT 例子中的第一部分 Header 有个 alg 属性,其值是 HS256,也就是 HMAC+SHA256 算法。
JWS 的全称是 JSON Web Signature ( RFC 7515[4] ) ,它的核心就是签名,保证数据未被篡改,而检查签名的过程就叫做验证。更通俗的理解,就是对应前面提到的 JWT 的第三部分 Signature,所以我才会说我们日常所使用的 JWT 都是 JWS。
通常在客户端-服务端模式中,JWS 使用 JWA 提供的 HS256 算法加上一个密钥即可,这种方式严格依赖密钥,但在分布式场景,可能多个服务都需要验证 JWT,若要在每个服务里面都保存密钥,那么安全性将会大打折扣,要知道,密钥一旦泄露,任何人都可以随意伪造 JWT。
解决办法就是使用非对称加密算法 RSA,RSA 有两把钥匙,一把公钥,一把私钥,可以使用私钥签发(签名分发)JWT ,使用公钥验证 JWT,公钥是所有人都可以获取到的。这样一来,就只有认证服务保存着私钥,进行签发,其他服务只能验证。
另一种 JWT 的实现:JWE
经过 Signature 签名后的 JWT 就是指的 JWS,而 JWS 仅仅是对前两部分签名,保证无法篡改,但是其 Payload(载荷)信息是暴露的(只是作了 base64UrlEncode 处理)。因此,使用 JWS 方式的 Payload 是不适合传递敏感数据的,JWT 的另一种实现 JWE 就是来解决这个问题的。
JWE 全称是 JSON Web Encryption ( RFC 7516[5] ) ,JWS 的 Payload 是 Base64Url 的明文,而 JWE 的数据则是经过加密的,它可以使 JWT 更加安全。
JWE 提供了两种方案:共享密钥方案和公钥/私钥方案。
共享密钥方案的工作原理是让各方都知道一个密钥,大家都可以签名验证,这和 JWS 是一致的。
而公钥/私钥方案的工作方式就不同了,在 JWS 中私钥对令牌进行签名,持有公钥的各方只能验证这些令牌;但在 JWE 中,持有私钥的一方是唯一可以解密令牌的一方,公钥持有者可以引入或交换新数据然后重新加密,因此,当使用公钥/私钥方案时,JWS 和 JWE 是互补的。
生产者对数据进行签名或加密,消费者可以对其进行验证或解密。对于 JWS,私钥对 JWT 进行签名,公钥用于验证,也就是生产者持有私钥,消费者持有公钥,数据流动只能从私钥持有者到公钥持有者。
相比之下,对于 JWE,公钥是用于加密数据,而私钥用来解密,在这种情况下,数据流动只能从公钥持有者到私钥持有者。如下图所示(来源 JWT Handbook[6] ):

相比于 JWS 的三个部分,JWE 有五个部分组成(四个小数点隔开)。一个 JWE 示例如下:
eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKxYxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTPcFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw
- Protected Header (受保护的头部) :类似于 JWS 的 Header ,标识加密算法和类型。
- Encrypted Key (加密密钥) :用于加密密文和其他加密数据的密钥。
- Initialization Vector (初始化向量) :一些加密算法需要额外的(通常是随机的)数据。
- Encrypted Data (Ciphertext) (加密的数据) :被加密的数据。
- Authentication Tag (认证标签) :算法产生的附加数据,可用于验证密文内容不被篡改。
这五个部分的生成,也就是 JWE 的加密过程可以分为 7 个步骤:
- 根据 Header alg 的声明,生成一定大小的随机数
- 根据密钥管理方式确定 Content Encryption Key (CEK)
- 根据密钥管理方式确定 JWE Encrypted Key
- 计算所选算法所需大小的 Initialization Vector (IV)。如果不需要,可以跳过
- 如果 Header 声明了 zip ,则压缩明文
- 使用 CEK、IV 和 Additional Authenticated Data (AAD,额外认证数据) ,通过 Header enc 声明的算法来加密内容,结果为 Ciphertext 和 Authentication Tag
- 最后按照以下算法构造出 Token:
base64(header) + '.' +
base64(encryptedKey) + '.' + // Steps 2 and 3
base64(initializationVector) + '.' + // Step 4
base64(ciphertext) + '.' + // Step 6
base64(authenticationTag) // Step 6
JWE 相比 JWS 更加安全可靠,但是不够轻量,有点复杂。
总结
- JWT是一种用于认证和安全传输信息的开放标准。它由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。
- 头部通常指定了签名所用的算法,如HMAC SHA256或RSA,负载包含了实际传输的数据。既可以只包含用户名等非敏感信息,也可以包含加密后的敏感信息。
- 对称式算法如HMAC需要预先双方共享密钥。非对称式算法如RSA可以使用私钥签名、公钥验证,更适合分布式场景。
- JWT标准包括JWS(签名)、JWE(加密)、JWK(密钥)等规范。JWE通过加密负载数据实现更高安全性。
- JWT作为无状态的认证方式,可避免服务端保存会话信息,常用于分布式微服务系统的认证和授权。
参考资料:
[1] RFC 7165
[2] RFC 7520
[3] RFC 7518
[4] RFC 7515
[5] RFC 7516
