转载自公众号寻寻觅觅的 Gopher
前言
最近在开发一个统一认证服务,涉及到 OIDC 协议,其中授权码模式所颁发的 id_token 使用的是 JWT(JSON Web Token)。
因为这次使用的库的默认签名算法和以往不同,所以特地去翻阅了 JWT 的 RFC 文档( RFC 7519[1] ),一番阅读后对 JWT 的核心内容有了更深入的理解。
你也可以在 Authing 官网上了解 JWT Token 的释义及使用,以及在 Authing 系统中验证 token 的方法。什么是 JWT Token
01
我们最常使用的 JWT
每次提到无状态的 JWT 时,相信都会看到另一种基于 Session 的用户认证方案介绍,这里也不例外,Session 的认证流程通常会像这样:
-
需要从内存或数据库里存取 session 数据
-
扩展性差,对于分布式应用,需要实现 session 数据共享
JWT 的用户认证流程颇具优势,将需要使用到的用户数据等信息放入 JWT 中,每次请求都会自动携带,只要保证密钥不泄露,JWT 就无法伪造。
一个简单的 JWT 示例如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIyMDIxLTEwLTI0IDAwOjAwOjAwIiwibmFtZSI6InRvZ2V0dG95b3UifQ.XdF46NflSUjnt-adAc6rNZEXI1OD6nxtwGuhz9qkxUA
可以看出 JWT 以不同颜色区分,通过两个小数点隔开,分为了三部分:
-
Header(头部):JSON 对象,描述 JWT 的元数据。其中 alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 属性表示这个令牌(token)的类型(type),统一写为 JWT。
-
Payload(载荷):JSON 对象,存放实际需要传递的数据,支持自定义字段。 -
Signature(签名):这部分就是 JWT 防篡改的精髓,其值是对前两部分 base64UrlEncode 后使用指定算法签名生成,以默认 HS256 为例,指定一个密钥(secret),就会按照如下公式生成:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret,
)
到这里,大多数人对 JWT 的认知应该是停留在此了,日常使用也已经足够,但你想更深入了解 JWT 的话,那你就得知道 JOSE 。
02
JOSE 规范
JOSE 全称 JSON Object Signing and Encryption ( RFC 7165[3] , RFC 7520[4] ),它定义了一系列的标准,用来规范网络传输过程中使用 JSON 的方式,我们上面一直说的 JWT 其实是 JOSE 体系之一。
其中 JWT 又可分为 JWS 和 JWE 这两种不同的实现,我们大部分日常所使用的,所说的 JWT 其实应该属于 JWS 。为什么这么说,请看下文。
03
JWA 和 JWS 以及 JWK
JWA 的全称是 JSON Web Algorithms ( RFC 7518[5] ) ,字如其名, JOSE 体系中涉及到的所有算法就是它来定义的,比如通用算法有 Base64-URL 和 SHA,签名算法有 HMAC,RSA 和 Elliptic Curve(EC 椭圆曲线)。
本文不会深入到算法原理,只是想让你知道 JWA 是做什么的。上述 JWT 例子中的第一部分 Header 有个 alg 属性,其值是 HS256,也就是 HMAC+SHA256 算法。
JWS 的全称是 JSON Web Signature ( RFC 7515[6] ) ,它的核心就是签名,保证数据未被篡改,而检查签名的过程就叫做验证。更通俗的理解,就是对应前面提到的 JWT 的第三部分 Signature,所以我才会说我们日常所使用的 JWT 都是 JWS。
通常在客户端-服务端模式中,JWS 使用 JWA 提供的 HS256 算法加上一个密钥即可,这种方式严格依赖密钥,但在分布式场景,可能多个服务都需要验证 JWT,若要在每个服务里面都保存密钥,那么安全性将会大打折扣,要知道,密钥一旦泄露,任何人都可以随意伪造 JWT。
解决办法就是使用非对称加密算法 RSA,RSA 有两把钥匙,一把公钥,一把私钥,可以使用私钥签发(签名分发)JWT ,使用公钥验证 JWT,公钥是所有人都可以获取到的。这样一来,就只有认证服务保存着私钥,进行签发,其他服务只能验证。
如下是一个使用 RS256 (RSA+SHA256) 算法生成的 JWT:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjF6aXlIVG15M184MDRDOU1jUENHVERmYWJCNThBNENlZG9Wa3VweXdVeU0ifQ.eyJqdGkiOiIzWUJ5eWZ2TDB4b01QNXdwTXVsZ0wiLCJzdWIiOiI2MDE5NDI5NjgwMWRjN2JjMmExYjI3MzUiLCJpYXQiOjE2MTI0NDQ4NzEsImV4cCI6MTYxMzY1NDQ3MSwic2NvcGUiOiJvcGVuaWQgZW1haWwgbWVzc2FnZSIsImlzcyI6Imh0dHBzOi8vc3RlYW0tdGFsay5hdXRoaW5nLmNuL29pZGMiLCJhdWQiOiI2MDE5M2M2MTBmOTExN2U3Y2IwNDkxNTkifQ.cYyZ6buwAjp7DzrYQEhvz5rvUBhkv_s8xzuv2JHgzYx0jbqqsWrA_-gufLTFGmNkZkZwPnF6ktjvPHFT-1iJfWGRruOOMV9QKPhk0S5L2eedtbKJU6XIEkl3F9KbOFwYM53v3E7_VC8RBj5IKqEY0qd4mW36C9VbS695wZlvMYnmXhIopYsd5c83i39fLBF8vEBZE1Rq6tqTQTbHAasR2eUz1LnOqxNp2NNkV2dzlcNIksSDbEGjTNkWceeTWBRtFMi_o9EWaHExdm5574jQ-ei5zE4L7x-zfp9iAe8neuAgTsqXOa6RJswhyn53cW4DwWg_g26lHJZXQvv_RHZRlQ
把它复制到 jwt.io 上面看看
注意我绿色框选中的地方,里面是一段 JSON,我们把它删掉,看看输入框的提示信息。
这里提示了,里面是填写公钥格式(通常为 PEM)或者 JWK(我们说过 RSA 算法是使用私钥签发 JWT,公钥进行验证),刚刚我们删掉的是一段 JSON,所以必然不是公钥格式,那是 JWK 吗?
当然是,JWK 的全称是 JSON Web Key ( RFC 7517[7] ) ,它就是一个 JSON,JWK 就是用 JSON 来表示密钥(JSON 字段因密钥类型而异)。例如刚才删除的 JWK:
{
"e": "AQAB",
"kty": "RSA",
"n": "wVKQLBUqOBiay2dkn9TlbfuaF40_edIKUmdLq6OlvzEMrP4IDzdOk50TMO0nfjJ6v5830_5x0vRg5bzZQeKpHniR0sw7qyoSI6n2eSkSnFt7P-N8gv2KWnwzVs_h9FDdeLOeVOU8k_qzkph3_tmBV7ZZG-4_DEvgvat6ifEC-WzzYqofsIrTiTT7ZFxTqid1q6zrrsmyU2DQH3WdgFiOJVVlN2D0BuZu5X7pGZup_RcWzt_9T6tQsGeU1juSuuUk_9_FVDXNNCTObfKCTKXqjW95ZgAI_xVrMeQC5nXlMh6VEaXfO83oy1j36wUoVUrUnkANhp-dnjTdvJgwN82dGQ"
}
其中 kty 字段是必须的,代表密钥类型,支持 EC 椭圆曲线密钥,RSA 密钥和 oct 对称密钥。
JWK 和公钥格式 Pem 是可以互相转换的:
我们现在已经知道,验证这个 JWT 是需要公钥或 JWK 的,那你会不会好奇 jwt.io 这个网站是怎么知道 JWK 的呢,为什么一粘贴,就自动将 JWK 填充进去了。
原理其实很简单,而且已经是一种大家都遵循的规范了,就是将 JWK 放在 iss/.well-known/jwks.json
下,其中 iss 就是 Payload 里面的 iss。
当你在 jwt.io 粘贴下 JWT 的瞬间,jwt.io 会先解析 Header,判断出 JWT 使用的算法(JWA),接着解析出 Payload 的信息,由于这里是 RS256 算法, 所以还会去请求 Payload 里的 iss 下的 .well-known/jwks.json
得到 JWK,从而完成 JWS 的验证。
04
另一种 JWT 的实现:JWE
我们说过,经过 Signature 签名后的 JWT 就是指的 JWS,而 JWS 仅仅是对前两部分签名,保证无法篡改,但是其 Payload(载荷)信息是暴露的(只是作了 base64UrlEncode 处理)。因此,使用 JWS 方式的 Payload 是不适合传递敏感数据的,JWT 的另一种实现 JWE 就是来解决这个问题的。
JWE 全称是 JSON Web Encryption ( RFC 7516[8] ) ,JWS 的 Payload 是 Base64Url 的明文,而 JWE 的数据则是经过加密的,它可以使 JWT 更加安全。
JWE 提供了两种方案:共享密钥方案和公钥/私钥方案。
共享密钥方案的工作原理是让各方都知道一个密钥,大家都可以签名验证,这和 JWS 是一致的。
而公钥/私钥方案的工作方式就不同了,在 JWS 中私钥对令牌进行签名,持有公钥的各方只能验证这些令牌;但在 JWE 中,持有私钥的一方是唯一可以解密令牌的一方,公钥持有者可以引入或交换新数据然后重新加密,因此,当使用公钥/私钥方案时,JWS 和 JWE 是互补的。
想要理解这一点的更简单的方法是从生产者和消费者的角度进行思考。
生产者对数据进行签名或加密,消费者可以对其进行验证或解密。对于 JWS,私钥对 JWT 进行签名,公钥用于验证,也就是生产者持有私钥,消费者持有公钥,数据流动只能从私钥持有者到公钥持有者。
相比之下,对于 JWE,公钥是用于加密数据,而私钥用来解密,在这种情况下,数据流动只能从公钥持有者到私钥持有者。如下图所示(来源 JWT Handbook[9] ):
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 更加安全可靠,但是不够轻量,有点复杂。
05
安全性考虑
总结
-
JOSE:规范网络传输过程中使用 JSON 的一系列标准 -
JWT:以 JSON 编码并由 JWS 或 JWE 安全传递的表示形式 -
JWS:签名和验证 Token -
JWE:加密和解密 Token -
JWA:定义 JOSE 体系中涉及到的所有算法 -
JWK:用 JSON 来表示密钥
希望今天的分享能让你更加了解 JWT!
参考资料
[1]RFC 7519:
https://datatracker.ietf.org/doc/rfc7519/
[2]jwt.io:
[3]RFC 7165:
https://www.rfc-editor.org/rfc/rfc7165.html
[4]RFC 7520:
https://www.rfc-editor.org/rfc/rfc7520.html
[5]RFC 7518:
https://www.rfc-editor.org/rfc/rfc7518.html
[6]RFC 7515:
https://www.rfc-editor.org/rfc/rfc7515.html
[7]RFC 7517:
https://www.rfc-editor.org/rfc/rfc7517.html
[8]RFC 7516:
https://www.rfc-editor.org/rfc/rfc7516.html
[9]JWT Handbook: