一文读懂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 的作用是:

  1. 更方便的传输和转换数据结构 JSON 格式经过 base64Url 编码。
  2. 数据是没加密的,没有经过隐藏或者混淆数据。
  3. 使用 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)

这个算法有如下几个特点:

  1. 无论输入多长,输出总是一定的。
  2. 输出无规律,输入改变一丁点就会导致输出完全不同,因此无法统计和猜测。
  3. 通过输入可以很容易地计算输出,但是,反过来,通过输出无法反推输入。相当于警察抓到一个人就很容易得到他的指纹,但仅仅采集到一个指纹信息,却很难知道这个指纹是谁的,长相如何等。

例如,对洋葱哈希算法这两个词进行某种哈希运算,得到的结果长度是一样的:

Hash("洋葱")    = 74c9227d95c45856683c457bbcd04d90
Hash("哈希算法") = 16600f689cdf9ad8305749bd64d3ca32
Hash("哈希算术") = ef1f60e4a59184a4f5bb9977a2245218
Hash("哈希算力") = a47bc40c3586cc0fbf275d2f83c17708
Hash("哈希算了") = d67394d8dcda13ca451ae72e90ed2de2

能实现这种特性的算法有很多,常见的有:MD5、SHA、Hmac等。

哈希算法的这种特性就可以用来验证某个文件是否被篡改。早期很多提供软件的公司,除了提供软件的下载地址,同时也会提供文件的摘要。

enter description here

比如你网速比较慢,不想去官网下载 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)

enter description here

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 体系之一。

img

其中 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] ):

img

相比于 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 更加安全可靠,但是不够轻量,有点复杂

总结

  1. JWT是一种用于认证和安全传输信息的开放标准。它由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。
  2. 头部通常指定了签名所用的算法,如HMAC SHA256或RSA,负载包含了实际传输的数据。既可以只包含用户名等非敏感信息,也可以包含加密后的敏感信息。
  3. 对称式算法如HMAC需要预先双方共享密钥。非对称式算法如RSA可以使用私钥签名、公钥验证,更适合分布式场景。
  4. JWT标准包括JWS(签名)、JWE(加密)、JWK(密钥)等规范。JWE通过加密负载数据实现更高安全性。
  5. JWT作为无状态的认证方式,可避免服务端保存会话信息,常用于分布式微服务系统的认证和授权。

参考资料:

[1] RFC 7165

[2] RFC 7520

[3] RFC 7518

[4] RFC 7515

[5] RFC 7516

mkjwk.org生成JWK

RSA算法原理一

RSA算法原理二

0%