有篇关于 JWT 的文章, 叫 "JWT: The Complete Guide to JSON web Tokens", 写得全面细致. 为了自己能更清晰理解并惠及更多人, 我把它大致翻译了过来, 有些地方稍显冗余就去掉了, 但还是接近八千字, 感谢原作者! 以下是正文:
本文的目标是让你学习 JWT 的工作原理和细节, 以及它在 Web 应用中能如何帮助你实现用户认证和会话管理功能. 那为什么需要深入理解 JWT 呢? 因为这样有助于你:
实现一个基于 JWT 的认证方案;
各种故障排查: 理解错误信息, 堆栈信息;
选择第三方库, 并理解他们的文档;
自己实现认证方案;
选择和配置第三方认证服务;
即使选择了某个基于 JWT 的认证方案, 同样还需要进行代码编写, 编码工作主要是在客户端, 但服务端也需要一些.
到本文的结尾, 你将深刻理解 JWT, 包括它基于的加密技术, 这种加密技术也广泛使用在其他安全案例中. 你还将明白什么时候该用 JWT 和为什么要使用, 同时理解 JWT 的数据格式, 可以使用各种线上工具去分析解决签名上遇到的问题.
为什么是 JWT?
相比在内存中存储随机 token 的用户会话管理方式来说, JWT 最大的优势是, 它使得将认证逻辑委托给第三方服务成为可能, 这些服务包括:
自己开发的, 中心化的认证服务器;
能生成 JWT 的 LDAP 服务;
完全是外部的第三方认证服务提供商, 比如 Auth0;
外部认证服务可以完全独立于我们自己的应用服务, 并且不需要通过网络共享任何密钥信息. 应用服务器不需要安装任何密钥, 减少了密钥丢失或者被盗窃的风险.
此外, 应用服务可以完全无状态, 因为不需要在多个请求之间将 token 存储在内存. 认证服务可以在生成 token 并返回给客户端后, 马上丢弃它! 同样, 密码摘要也没有必要保存在应用数据库中, 因此减少了被盗的风险和其它安全相关的 bug.
此刻也许你心里想: 我有一个内部应用, 对此, JWT 是一个好的方案吗? 是的, 在本文的最后一节里, 我们将讨论 JWT 在这种典型场景下的使用情况.
目录
本文我们将讨论以下这些话题:
什么是 JWT?
JWT 在线验证工具;
JWT 的格式;
JWT 的核心要素: Header, Payload, Signature;
Base64Url (vs Base64);
使用 JWT 进行会话管理: 主题和期限
HS256 签名 - 它是如何工作的?
数字签名;
哈希函数和 SHA-256;
RS256 JWT 签名 - 谈谈公钥加密;
RS256 vs HS256 签名 - 哪种方式更好?
JWKS (JSON Web Key Set) 端点(Endpoints);
如何实现 JWT 签名的周期性键旋转(Periodic Key Rotation);
JWT 在企业应用中的使用;
归纳和结论;
JWT 是什么?
JSON Web Token (or JWT)只是一个包含某种意义数据的 JSON 串. 它最重要的特性就是, 为了确认它是否有效, 我们只需要看 JWT 本身的内容, 而不需要借助于第三方服务或者在多个请求之间将其保存在内存中 - 这是因为它本身携带了信息验证码 Mac(Message Authentication Code).
一个 JWT 包含 3 个部分: 头部 Header, 数据 Payload, 签名 Signature. 让我们逐个来了解一下, 先从 Payload 开始吧.
JWT Payload 看起来是怎样的呢?
Payload 只是一个普通的 JavaScript 对象. 对于 payload 的内容, JWT 是没有任何限制的, 但必须注意的是, JWT 是没有加密的. 因此, 任何放在 token 里面的信息, 如果被截获了, 对任何人别人是可读的. 因此, 我们不应该在 Payload 里面存放任何黑客可以利用的用户信息.
JWT Header - 为什么是必须的?
Payload 的内容在接收者端是通过签名 (Signature) 来校验的. 不过存在多种类型的签名, 因此, 接收者需要知道使用的是哪种类型的签名.
这种关于 token 本身的元数据信息存放在另外的 JavaScript 对象里面, 并随着 Payload 一起发送给客户. 这个独立的对象就是一个 JSON 对象, 叫 JWT Header, 它也是普通的 JavaScript 对象, 在这里面我们可以看到签名类型信息, 比如 RS256.
JWT signatures - 如何被使用来完成认证的?
JWT 的最后一部分是签名, 它也叫信息验证码 Mac. 签名只能由拥有 Payload,Header 和密钥的角色生成.
那签名是如何完成认证功能的呢, 且看:
用户向认证服务器提交用户名和密码, 认证服务器也可以和应用服务器部署在一起, 但往往是独立的居多;
认证服务器校验用户名和密码组合, 然后创建一个 JWT token,token 的 Payload 里面包含用户的身份信息, 以及过期时间戳;
认证服务器使用密钥对 Header 和 Payload 进行签名, 然后发送给客户浏览器;
浏览器获取到经过签名的 JWT token, 然后在之后的每个 HTTP 请求中附带着发送给应用服务器. 经过签名的 JWT 就像一个临时的用户凭证, 代替了用户名和密码组合, 之后都是 JWT token 和应用服务器打交道了;
应用服务器检查 JWT 签名, 确认 Payload 确实是由密钥拥有者签过名的;
Payload 身份信息代表了某个用户;
只有认证服务器拥有私钥, 并且认证服务器只把 token 发给提供了正确密码的用户;
因此应用服务器可以认为这个 token 是由认证服务器颁发的也是安全的, 因为该用户具有了正确的密码;
应用服务器继续完成 HTTP 请求, 并认为这些请求确实属于这个用户;
这样的话, 黑客假扮合法用户的办法要么是盗到了用户名和密码组合, 要么盗到了认证服务器上的签名私钥.
正如我们所见, 签名的确是 JWT 的关键部分!
签名使得无状态的服务器只需要通过查看 HTTP 请求中的 JWT token 就能保证 HTTP 请求是来自某个用户, 而不需要每次请求时都发送密码.
JWT 的目标是让服务器无状态?
实际上, JWT 真正的好处是让认证服务器和校验 JWT token 的应用服务器可以完全分开, 而让服务器无状态化只是它的一个副作用罢了. 这意味着应用服务器只需要最简单的认证逻辑 - 校验 JWT! 我们可以将整个应用集群的登录 / 注册委托给一个单独的认证服务器. 这也意味着应用服务器更简单更安全, 因为更多的认证功能集中部署在认证服务器, 可以被跨应用使用.
到此, 我们从更高的层面了解了 JWT 是怎样完成无状态的第三方认证, 接下来让我们了解它的实现细节.
JSON Web Token 看起来是怎样的呢?
让我们来看看 JWT 的实际例子, 这是从 jwt.io 的 JWT 校验工具得到的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
你可能会想, JSON 对象去哪了啊? 不过, 你会马上找回它.
我们可以看到, 这个 JWT 包含 3 部分, 是由 "." 号分开的. 第一部分是 JWT 的 Header:
- JWT Header:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
第二部分是 Payload:
- JWT Payload:
- eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
第三方部分是签名 Signature:
- JWT Signature:
- TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
如果你还想确认这些信息是否真的存在, 可以拷贝 JWT 串到 jwt.io 的在线校验工具校验一下即可.
但这些字符串代表什么意思呢? 我们如何取回 JWT 的信息?
Base64, 抑或是 Base64Url?
不管你信不信, 现在的 Payload,Header 和 Signature 还是可读的. 这只是因为我们不想在网络传输过程中出现一些垃圾文本, 比如这样的字符串: qîüö:Ã.
这是因为世界上不同的计算机以不同的编码方式处理字符串, 比如 UTF-8,ISO 8859-1, 等等. 因此, 这种问题到处存在, 只要我们在某个平台上用到一个字符串, 它总是使用了某种编码方式, 即使我们没有显示指定:
操作系统的默认编码方式;
服务器上的配置的编码参数;
我们希望在网络上传输字符串时没有这些问题, 那就需要选择这些字符的一个子集, 对于这个子集, 几乎所有的编码方式都是一样的处理方式, 这就是 Base64 编码方式产生的原因.
Base64 vs Base64Url
但是我们在 JWT 看到的并不是 Base64, 实际上是 Base64Url, 它和 Base64 类似, 但有一些字符不一样, 因此我们可以将 JWT 作为 URL 的参数进行传递.
我们看一下 Payload 部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
我们使用在线解码器来解析它, 就得到了一个 JSON 对象, 因此, 我们可以得到这样的结论: JWT Header 和 Payload 的内容是普通的 JavaScript 对象, 转换成 JSON 并进行 Base64Url 编码, 以 "." 号隔开.
在学习签名 Signature 之前, 我们先来看看在实际的用户认证案例中, 我们是将哪些内容放入 Payload 中的.
基于 JWT 的用户会话管理: 主题和期限
之前有提到, JWT 的 Payload 理论上可以存放任何内容, 不一定是用户身份信息, 只不过使用 JWT 作为认证是最常用的方式. Payload 还有一些特定的属性来支持:
用户身份
会话过期
这里是 Payload 的几个最常用的标准属性:
. iss 代表生成 token 的实体, 一般就是认证服务器
. iat 创建 JWT 的时间戳(in seconds since Epoch)
. sub 包含用户的身份信息
. exp token 的过期时间戳
我们把这叫做 Bearer Token, 意思是:
认证服务器确认这个 token 的持有者是具有由 sub 属性表示的 ID 的用户, 因此可以放行.
现在我们理解了 Payload 在用户认证中是如何使用的, 接下来我们来了解一下签名 Signature. 对于 JWT, 签名方式有很多种, 这里我们主要了解 HS256 和 RS256.
我们先来看看 HS256.
HS256 JWT 数字签名 - 它是如何工作的?
和很多签名方法一样, HS256 数字签名基于一种特殊的函数: 加密哈希函数.
这听起来有点唬人, 不过是个值得学习的概念: 这个知识已经被使用了 20 多年并还会持续很长时间. 很多关于安全的实现都围绕着哈希, 它在 Web 安全中随处可见.
我们将分为两步, 首先要了解什么是哈希函数, 然后再看通过这样的函数和密码, 如何生成信息认证码(Message Authentication Code), 也就是数字签名.
什么是哈希函数(Hashing function)?
哈希函数是一种特殊的函数: 它在数字签名中有很多实际的使用案例. 现在我们将谈论它四个有趣的属性, 然后看看这些属性如何使得我们可以生成可校验的签名. 这里我们将使用的哈希函数是: SHA-256.
哈希函数属性 1 - 不可逆性
哈希函数有点类似绞肉机: 你把牛排放入一端, 然后从另一端得到汉堡包, 你再也无法从汉堡包取回牛排了. 因此, 函数是完全不可逆的. 这就意味着我们把 Header 和 Payload 作用于这个函数后, 没有人可以从函数输出的信息中取回 Header 和 Payload 的原始值.
使用在线的哈希计算器, 我们可以看到 SHA-256 的一个输出值如下:
3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3
同时, 哈希并不是加密, 加密在定义中是可逆的, 我们总是需要从加密后的信息中得到原始信息.
哈希函数属性 2 - 可重复生成
另外一个需要知道的是, 哈希函数是可重复生成信息的, 也就是如果我们输入同样的 Header 和 Payload 信息, 每次得到的结果是完全一样的. 这就意味着, 给定输入组合和哈希输出值, 我们总是可以校验该输出值 (比如签名 signature) 的正确性, 因为我们可以重新计算(我们有输入值的情况下).
哈希函数属性 3 - 没有冲突
还有一个属性是, 如果我们提供不同的输入值, 总是得到不同的唯一的输出值. 这就意味着我们将哈希函数作用于某个 Payload 和 Header 之后, 总是得到相同的结果, 其它输入值组合不会得到和这一样的结果, 因此, 哈希函数的不同输出值就代表了输入值的不同.
哈希函数属性 4 - 不可预测性
哈希函数的最后一个属性就是不可预测性, 给定一个输出值, 无法通过各种手段猜测到输入值. 假设我们尝试从上面的输出值中找到生成它的 Payload, 我们只能猜测输入值然后对比输出值看看是否匹配.
但对于哈希函数来说, 这种方式是不可行的:
这是因为在哈希函数中, 即使你改变了一个输入字符甚至一个比特值, 输出中一般有 50% 的比特值都会被改变, 输入值小小的变动, 可能会得到完全不同的输出值.
这些听起来挺有趣的, 不过你可能又在想了: 哈希函数是怎样完成数字签名的呢? 黑客是否可以拿着 Header 和 Payload, 而不管 Signature 呢? 任何人都可以使用 SHA-256 哈希函数生成一个输出, 然后附加到 JWT 的 signature 部分, 对吧?
怎样使用哈希函数生成签名?
这是正确的, 任何人都可以使用哈希函数, 然后输入 Header 和 Payload 来生成结果. 但 HS256 签名不止这样, 我们拿到 Header,Payload 外, 还要加上一个密码, 将这三个输入值一起哈希. 输出结果是一个 SHA-256 HMAC 或者基于哈希的 Mac. 如果需要重复生成, 则需要同时拥有 Header,Payload 和密码才可以. 这也意味着, 哈希函数的输出结果是一个数字签名, 因为输出结果就表示了 Payload 是由拥有密码的角色生成并加签了的, 没有其它方式可以生成这样的输出值了.
将哈希结果附加到消息上, 是为了让接收者可以验证. 哈希结果叫 HMAC:Hash-Based Message Authentication Code, 是数字签名的一种形式. 这就是我们在 JWT 中所做的, JWT 的第三部分是由 Header,Payload 通过 SHA-256 函数生成, 并使用 Base64Url 进行编码.
如何校验 JWT 签名?
当我们的服务接收到 HS256 签名的 JWT 时, 我们需要使用同样的密码才能校验并确认 token 里面的 Payload 是否有效. 为了验证签名, 我们只需要将 JWT Header 和 Payload 以及密码通过哈希函数生成结果. 如果是使用 HS256 函数, JWT 的接收者需要拿到和发送者一样的密码值. 如果我们得到的哈希结果和 JWT 第三部分的签名值是一致的, 则说明有效, 可以确认发送者确实和接收者拥有相同的密码值.
而数字签名和 HMAC 又是如何工作的呢?
手动确认 SHA-256 JWT 签名
我们从之前的 JWT 中去掉签名和第二个 ".", 只留下 Header 和 Payload 部分. 看起来如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
现在如果你拷贝这个字符串到在线的 HMAC SHA-256 工具, 并使用上密码, 就可以取回 JWT 签名. 或者, 你会得到 Base64 编码后的内容, 后面还有一个 "=" 字符, 这算是已经接近 Base64Url:
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=
那个 "=" 在 URL 栏中会显示为 "=", 会显得混乱, 这也解释了在我们把 JWT 拼接到 URL 发送时, 需要 Base64Url 的原因.
为什么需要其它的签名类型呢?
以上解释了 JWT 签名是如何应用于认证的, HS256 只是一种具体的签名类型. 其它的签名类型中, 最常用的是: RS256.
有什么区别呢? 我们介绍 HS256 只是为了更容易理解 Mac 码的概念, 你可能也会发现它在一些生产环境的应用中被使用. 但是一般来说, 使用 RS256 签名方式会更好, 下一节我们将看到, RS256 相对于 HS256 来说有诸多优势.
HS256 签名方式的劣势
如果输入的密码相对弱的话, HS256 可能会被暴力破解, 基于密钥的技术都有这个问题. 更甚的是, HS256 要求 JWT 的生产者和消费者都预先拥有相同的密码.
不切实际的密码分发
这意味着我们在修改密码后, 需要把它分发并安装到所有需要它的网络节点. 这不仅不方便, 而且容易出错, 还涉及到服务器间的协调和暂停服务问题. 如果服务器是由另外的团队维护, 比如第三方组织, 这种方式就更不可行了.
Token 的创建和校验没有分离
创建和校验 JWT 的能力没有区分开, 使用 HS256 时, 网络的任何人都可以创建和校验 token, 因为他们都有密码. 这就意味着密码可能会从更多的地方丢失或者受攻击, 因为密码到处分发, 而并不是每个应用都具有一样的安全保护机制.
弥补这问题的一个方法是, 创建一个共享的密码给每一种类型的应用. 不过, 我们马上要学习新的签名方式, 这个签名方式解决了以上所有的问题, 并且目前所有基于 JWT 的方案都默认使用的, 那就是 RS256.
RS256 JWT 签名
使用 RS256 我们同样需要生成一个 Mac, 其目的仍然是创建一个数字签名来证明一个 JWT 的有效性. 只是在这种签名方式中就, 我们将创建 token 和校验 token 的能力分开, 只有认证服务器具备创建的能力, 而应用服务器, 具备校验的能力.
这样, 我们需要创建两个密钥而不是一个:
仍然需要一个私钥, 不过这次它只能被认证服务器拥有, 只用来签名 JWT.
私钥只能用来签名 JWT, 不能用来校验它.
第二个密钥叫做公钥(public key), 是应用服务器使用来校验 JWT.
公钥可以用来校验 JWT, 但不能用来给 JWT 签名.
公钥一般不需要严密保管, 因为即便黑客拿到了, 也无法使用它来伪造签名.
RSA 加密技术介绍
RS256 使用一种特殊的密钥, 叫 RSA 密钥. RSA 是一种加解密算法, 使用一个密钥进行加密, 然后用另外一个密钥解密. 值得注意的是, RSA 不是哈希函数, 从定义上来说, 这种方式加密是可逆的, 也就是我们可以从加密后的内容得到原始内容.
来看一下 RSA 公钥是怎样的:
- --BEGIN PUBLIC KEY--
- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
- --END PUBLIC KEY--
乍一看有点古怪, 但它是使用命令行工具比如 openssl 或者在线的 RSA 密钥生成工具来生成的.
这个公钥是公开发布的, 因此黑客根本不需要猜测, 他本来就可以拥有它.
但这里还有一个 RSA 私钥:
- --BEGIN RSA PRIVATE KEY--
- MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQABAoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5CpuGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0KSu5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==
- --END RSA PRIVATE KEY--
好消息是, 黑客没有任何办法猜测私钥.
而且, 这两个密钥是相关的, 一个密钥加密的内容只能由另外的密钥来解密. 那我们又如何用它们生成签名呢?
为什么不用 RSA 加密 Payload 就完了?
现在尝试着使用 RSA 来生成一个数字签名:
我们使用 Header 和 Payload, 然后使用私钥对其进行 RSA 加密, 最后返回 JWT.
接收者拿到 JWT 后, 使用公钥解密, 然后检查解密后的值. 如果解密过程顺利并且其输出是一个 JSON 值, 往往就意味着该 JWT 就是认证服务器创建并加密了的.
相比哈希函数, RSA 加密过程比较慢. 对于数据比较大的 Payload 来说, 可能会是个问题.
那 HS256 签名方式在实际中又是如何使用 RSA 的呢?
使用 RSA 和 SHA-256 签名 JWT (RSA-SHA256)
在实际中, 我们一般先将 Header 和 Payload 一起哈希, 比如使用 SHA-256. 这个速度是很快的, 这样我们就得到了一个代表输入数据的唯一表示, 比实际输入数据要小得多.
然后我们使用 RSA 对哈希结果而不是完整的数据 (Header+Payload) 进行加密, 就得到了 RS256 签名. 我们将这个签名附加到 JWT 的第三部分, 然后返回给客户端.
接收者是怎样检查 RS256 签名的?
接收者将:
取出 Header 和 Payload, 然后使用 SHA-256 进行哈希.
使用公钥解密数字签名, 得到签名的哈希值.
接收者将解密签名得到的哈希值和刚使用 Header 和 Payload 参与计算的哈希值进行比较. 如果两个哈希值相等, 则证明 JWT 确实是由认证服务器创建的.
任何人都可以计算哈希值, 但只有认证服务器可以使用 RSA 私钥对其进行加密.
接下来我们学习一下在 RS256 签名中遇到问题时的解决思路.
手工确认 RS256 JWT 签名
我们看下 jwt.io 上的例子, 一个使用 RS256 加签的 JWT.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE
从表面上看, 这和 HS256 JWT 没有多大区别, 但这是使用前面展示的同一个 RSA 私钥加签了的.
我们把签名部分去掉, 只看 Header 和 Payload:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
现在我们需要做的就是使用 SHA-256 对其进行哈希, 然后使用上面的 RSA 私钥进行加密. 这样得到的结果就是 JWT 签名, 我们可以使用 Node 的内嵌模块 Crypto 来确认.
首先, 我们把 RSA 私钥保存到一个文本文件, 比如叫 private.key. 然后在命令行运行 node shell, 执行一个小程序, 得到下面的结果:
EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=
这个结果和 JWT 签名完全不同, 不过等等, 这里面还有斜杠和等号. 如果不进一步处理, 这是没法放到 URL 里面的.
这是因为我们生成的是 Base64 版本的签名, 而我们真正需要的是 Base64Url 版本的. 我们尝试转换一下:
- bash$ node
- const base64url = require('base64url');
- base64url.fromBase64("EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=");
得到下面的结果:
EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE
这就是我们真正想要创建的 RS256 签名了! 也证明了我们对 RS256 JWT 签名的理解是正确的, 而且我们知道了在遇到问题时如何去分析解决.
总之, RS256 JWT 签名就是使用 RSA 对 Header 和 Payload 的哈希值进行加密的结果. 现在我们知道 RS256 签名是如何工作的了, 但这些签名为什么好于 HS256 签名呢?
RS256 签名 vs HS256 - 为什么使用 RS256?
使用 RS256, 黑客可以轻松实现创建签名的第一步, 即根据盗来的 JWT Header 和 Payload 生成 SHA-256 哈希值, 之后他还要暴力破解 RSA 才能继续生成签名.
但这还不是我们为什么选择 RS256 而不是 HS256 的客观原因.
我们知道, 使用 RS256 时, 私钥只有认证服务器持有, 这就安全得多 - 加签密钥丢失的风险降低了. 然而选择 RS256 更重要的理由是 - 简化密钥分发.
如何进行密钥分发部署
还记得之前我们说过, 用来校验 token 的公钥可以随意分发, 黑客无法使用它来做任何有意义的事情. 然而黑客并不是想校验 token, 他们只是想伪造它们. 这就使得我们将公钥放置到受我们自己控制的服务器上成为可能.
应用服务器连接到公钥放置的服务器获取公钥, 然后定期检查公钥是否有变化. 因此, 在更新密钥时, 应用服务器和认证服务器不需要同时暂停服务.
那公钥又如何分发呢? 下面是一种可行的格式.
JSON Web Key Set Endpoints
有多种发布公钥的格式, 但这里有一种较为熟悉: JWKS, 全称 JSON Web Key Set.
如果你好奇这些 endpoints 看起来是怎样的, 可以看一下这个线上例子 live example, 下面这个是我们从 HTT GET 请求得到的回复:
Kid 是密钥身份, x5c 是某种公钥的表示法. 这种格式的优点是其标准化, 我们只需要知道 endpoint 的 URL, 和一个可以解析 JWKS 的库, 就可以使用公钥来校验 JWT 了, 而不需要安装到自己的服务器.
JWT 常常使用在公共网站上, 以及社交产品的登录方案中. 对于内部系统, 它是怎么被使用的呢?
JWT 在企业中的应用
JWT 同样适用于企业内部, 替代经典的存在已知安全隐患的预身份验证设置 (Pre-Authentication setup) 方式.
预身份验证设置方式中, 我们的应用服务器在私有网络的一个代理后面运行, 然后从 HTTP 请求头中获取当前用户信息. 代表用户身份的 HTTP 请求头通常由中心化的登录页面填充, 同时中心化的节点也对用户 session 进行管理.
当 session 过期后, 服务器将阻止对应用的访问, 并要求用户重新登录认证. 之后, 它将所有请求转发到应用服务器并在 HTTP 请求头添加代表用户身份的信息.
问题是这种设置方式, 内网上的任何人都可以假扮成某个用户, 只要设置同样的 HTTP 请求头.
对此也有一些解决方案, 比如白名单列表, 或者某种客户凭证.
更好的预身份验证设置方式
预身份验证设置方式是一个好主意, 毕竟这种方式可以使得应用开发者不需要实现认证逻辑, 减少开发时间和潜在的安全问题.
如果能有预身份验证设置方式的便捷, 又没有安全方面的妥协, 岂不美哉?
如果我们考虑到 JWT, 则可以轻松做到. 我们不像以往那样将用户名放到 HTTP 请求头, 而是将 HTTP 请求头封装成一个 JWT. 我们将用户名放到 Payload 里面, 再由认证服务器加签.
应用服务器不再从 HTTP 请求头获取用户名, 而是首先校验 JWT:
如果签名是正确的, 则用户认证通过, 请求可以放行;
否则, 应用服务器简单的拒绝请求就好了;
这样的结果就是, 即使在私有网络内, 我们的认证功能也可以正常工作. 我们再也不需要通过 HTTP 请求头来识别用户了, 我们保证了 HTTP 请求头的有效性并且是由代理生成的, 而不是某黑客试图以某个用户身份登录.
总结
通过本文, 我们对 JWT 是什么有了一个全面的了解, 以及它是如何在认证中被使用的. JWT 只是一个简单的 JSON 对象, 并且易于验证, 难于伪造.
此外, JWT 并不是一定要用来做认证的, 我们可以使用 JWT 在网络上发送各种数据.
另外一个和安全相关的使用 JWT 的情况是授权: 我们可以在 Payload 里面放置用户的角色列表, 比如只读用户, 管理员等等, 对用户在应用服务器上的行为进行限制.
好了, 本文到此结束, 祝你阅读愉快!
来源: https://www.cnblogs.com/woshiweige/p/11068626.html