在此下载关于 JSON Web Token (JWT) 不安全的电子书
有时,人们会拿起那些旨在解决狭窄问题的技术,并开始将其广泛应用。问题可能看起来相似,但使用独特的单点技术来解决普遍问题可能会产生意想不到的后果。打个比方,如果你是一把锤子,看什么都像钉子。JWT 就是这样一种技术。
来自 Okta 等公司的领域专家撰写了许多深入文章和制作了视频,讨论了使用 JWT token 的潜在危险和低效性[1]。然而,这些警告却被营销人员、YouTube 博主、博主、课程创建者以及其他一些或有意或无意地推广它的人所掩盖。
如果你看看许多这些视频和文章,它们都只谈论了 JWT 的感知到的好处,而忽略了其不足之处。更具体地说,他们只展示了如何使用它,但没有讨论在实际生产环境中 JWT 带来的撤销和额外复杂性。他们也从未将其与现有经过实战检验的方法进行深入比较,以真正权衡利弊。
或者也许是它那个完美、引人注目且友好的名字导致了它的流行。“JSON”(普遍受欢迎)、“Web”(用于 Web)和“Token”(暗示无状态)让人们认为它非常适合他们的 Web 认证工作。
所以我认为这是营销战胜了工程师和安全专家的情况。但是,这并非全是坏事,因为在 Hacker News 上有关于 JWT 的定期、长时间且充满激情的辩论(参见此处、此处和此处),所以仍有希望。
仔细想想,这些持续的辩论本身就应该是一个危险信号,因为你不应该看到这样的辩论,尤其是在安全领域。安全性应该是二元的。一种技术要么是安全的,要么不是。
无论如何,在这篇博客文章中,我将重点介绍使用 JWT 的潜在危险,并讨论一个已经存在了十年的经过实战检验的解决方案。
为了进一步理解,当我谈论 JWT 时,我指的是“无状态 JWT”,这是 JWT 流行的主要原因,也是最初使用 JWT 的最大原因。此外,我在下面的资源部分列出了所有其他深入探讨 JWT 细节的文章。
在我们理解为什么它危险之前,首先通过一个示例用例来理解它是如何工作的。
想象一下你在使用 Twitter。你登录,写一条推文,点赞一条推文,然后转发别人的推文。所以你做了四个操作。对于每个操作,你需要先进行身份验证和授权,然后才能执行该特定操作。
下面是传统方法中发生的情况。
问题在于第四步很慢,并且用户执行的每一个操作都需要重复这一步骤。因此,每一次 API 调用都会导致至少两次缓慢的数据库调用,这会降低整体响应时间。
有不同的方法可以实现这一点。
现在来看看使用 JWT 的方法。
JWT,特别是当用作会话时,试图通过完全消除数据库查询来解决问题。
主要思想是将用户信息存储在会话 token 本身中!因此,不是一个长的随机字符串,而是将实际用户信息存储在会话 token 本身中。为了安全起见,使用只有服务器知道的密钥对 token 的一部分进行签名。
因此,即使客户端和服务器可以看到 token 的用户信息部分,第二部分,即签名部分,只能由服务器验证。
在下图中,token 的粉色部分包含有效负载(用户信息),客户端或服务器都可以看到。
但蓝色部分是使用一个密钥字符串、头部信息以及有效负载本身进行签名的。因此,如果客户端篡改了有效负载(例如冒充不同的用户),签名就会不同,并且无法通过身份验证。
以下是使用 JWT 时的用例情景
今后,对于用户的每一个操作,服务器只需验证签名部分,获取用户信息,然后让用户执行该操作。从而完全跳过了数据库调用。
但关于 JWT token 还有一件额外且重要的事情需要了解。那就是它使用一个过期时间来使自己过期。通常设置为 5 分钟到 30 分钟。而且由于它是自包含的,你无法轻松地撤销/使其失效/更新它。这才是问题的症结所在。
JWT 最大的问题是 token 撤销问题。由于它会一直有效直到过期,服务器没有简单的方法来撤销它。
以下是一些可能使这变得危险的用例。
想象一下你在发推后退出了 Twitter。你可能会认为你已经退出了服务器,但事实并非如此。因为 JWT 是自包含的,它会一直工作直到过期。这可能是 5 分钟或 30 分钟,或者 token 中设置的任何时长。因此,如果有人在这段时间内获取了该 token,他们可以继续访问,直到它过期。
想象一下,你是 Twitter 或某个在线实时游戏的版主,该系统中有很多真实用户。作为版主,你想快速阻止某人滥用系统。你做不到,原因还是一样。即使你阻止了该用户,他们仍将继续访问服务器,直到 token 过期。
想象一下,用户是管理员,但权限被降级为普通用户。这同样不会立即生效,用户将继续是管理员,直到 token 过期。
已经发现许多实现 JWT 的库多年来存在许多安全问题,甚至规范本身也存在安全问题。即使是推广 JWT 的 Auth0 自己,也也曾遇到过问题。
在许多复杂的实际应用中,你可能需要存储大量不同的信息。将其存储在 JWT token 中可能会超出允许的 URL 长度或 cookie 长度,从而导致问题。此外,你现在可能在每次请求中发送大量数据。
在许多实际应用中,服务器必须维护用户的 IP 并跟踪 API 以进行限速和 IP 白名单。因此,无论如何你都需要使用一个速度极快的数据库。认为你的应用可以通过 JWT 变成无状态的,这根本不现实。
一个流行的解决方案是将一个“已撤销 token”列表存储在数据库中,并在每次调用时进行检查。如果 token 在已撤销列表中,则阻止用户执行下一步操作。但这样一来,你又需要额外调用数据库来检查 token 是否已撤销,从而完全违背了使用 JWT 的初衷。
虽然 JWT 确实消除了数据库查询,但它在此过程中引入了安全问题和其他复杂性。安全性是二元的——要么安全,要么不安全。因此,将 JWT 用于用户会话是危险的。
在一些场景中,你在后端进行服务器到服务器(或微服务到微服务)通信,一个服务可以生成一个 JWT token 发送给另一个服务用于授权目的。还有一些其他狭窄的场景,比如重置密码,你可以在其中发送一个 JWT token 作为一次性、短时有效的 token 来验证用户的电子邮件。
解决方案是完全不将 JWT 用于会话目的。而是以更高效的方式采用传统的、但经过实战检验的方法。
也就是说,让数据库查询速度快得惊人(亚毫秒级),以至于额外的调用完全没有影响。
有没有哪个数据库的速度快得惊人,可以在亚毫秒内处理数百万个请求?
当然有。它叫做 Redis! 每天服务数十亿用户的成千上万家公司正是为了这个目的而使用 Redis!
而 Redis Enterprise 是 Redis OSS 的增强版本,提供 99.999% 的可用性,并且可以处理数万亿个请求。它可以在私有云上作为免费软件使用,或者在任何一家顶级的云服务提供商上使用。
还有什么?Redis Enterprise 现在已经从仅仅是一个缓存或会话存储,发展成为一个功能齐全的多模型数据库,拥有其模块生态系统,与 Redis 核心原生运行。 例如,你可以使用 JSON(比市场领导者快 10 倍),从而拥有一个实时类似 MongoDB 的数据库,或者使用搜索和查询功能(快 4-100 倍)并实现像 Algolia 那样的实时全文搜索。
如果你只将 Redis 用作会话存储,并将其他数据库用作主数据库,那么你的架构将如下所示。 需要注意的是,Redis Enterprise 提供了四种类型的缓存:Cache-aside(延迟加载)、Write-Behind(写回)、Write-Through(写穿)和 Read-replica(读副本),而 Redis OSS 只提供一种(Cache-aside)。
请注意,闪电表情符号表示速度极快。蜗牛表情符号表示速度慢。
如前所述,你也可以将 Redis 用作整个数据层的主数据库。在这种情况下,你的架构将变得更加简单,基本上一切都会变得极快。
当然,公司不仅将 Redis 用作独立的数据库,还用作地理分布式数据库集群。