dot Redis 8 已推出——并且是开源的

了解更多

JSON Web Token (JWT) 对用户会话来说是危险的——这里有一个解决方案

在此下载关于 JSON Web Token (JWT) 不安全的电子书

有时,人们会拿起那些旨在解决狭窄问题的技术,并开始将其广泛应用。问题可能看起来相似,但使用独特的单点技术来解决普遍问题可能会产生意想不到的后果。打个比方,如果你是一把锤子,看什么都像钉子。JWT 就是这样一种技术。

来源:“停止将 JWT 用于会话”(链接见下方参考资料)。
sessions
来源:“为什么 JWT 不利于身份验证”——Randall Degges,Okta 开发者布道负责人。
not default
来源:“JWT 不应成为会话的默认选项”(链接见下方参考资料)。
Ptacek

来自 Okta 等公司的领域专家撰写了许多深入文章和制作了视频,讨论了使用 JWT token 的潜在危险和低效性[1]。然而,这些警告却被营销人员、YouTube 博主、博主、课程创建者以及其他一些或有意或无意地推广它的人所掩盖。 

如果你看看许多这些视频和文章,它们都只谈论了 JWT 的感知到的好处,而忽略了其不足之处。更具体地说,他们只展示了如何使用它,但没有讨论在实际生产环境中 JWT 带来的撤销和额外复杂性。他们也从未将其与现有经过实战检验的方法进行深入比较,以真正权衡利弊。 

或者也许是它那个完美、引人注目且友好的名字导致了它的流行。“JSON”(普遍受欢迎)、“Web”(用于 Web)和“Token”(暗示无状态)让人们认为它非常适合他们的 Web 认证工作。

所以我认为这是营销战胜了工程师和安全专家的情况。但是,这并非全是坏事,因为在 Hacker News 上有关于 JWT 的定期、长时间且充满激情的辩论(参见此处此处此处),所以仍有希望。 

仔细想想,这些持续的辩论本身就应该是一个危险信号,因为你不应该看到这样的辩论,尤其是在安全领域。安全性应该是二元的。一种技术要么是安全的,要么不是。

无论如何,在这篇博客文章中,我将重点介绍使用 JWT 的潜在危险,并讨论一个已经存在了十年的经过实战检验的解决方案。 

为了进一步理解,当我谈论 JWT 时,我指的是“无状态 JWT”,这是 JWT 流行的主要原因,也是最初使用 JWT 的最大原因。此外,我在下面的资源部分列出了所有其他深入探讨 JWT 细节的文章。

在我们理解为什么它危险之前,首先通过一个示例用例来理解它是如何工作的。

用例

想象一下你在使用 Twitter。你登录,写一条推文,点赞一条推文,然后转发别人的推文。所以你做了四个操作。对于每个操作,你需要先进行身份验证和授权,然后才能执行该特定操作。 

下面是传统方法中发生的情况。

传统方法

  1. 你使用用户名和密码登录
    1. 服务器首先验证用户身份。
    2. 然后服务器创建一个会话 token,将该 token 和用户信息一起存储到某个数据库中。(注意:会话 token 是一个长的、不可识别的字符串——也称为不透明字符串——看起来像这样:fsaf12312dfsdf364351312srw12312312dasd1et3423r)
  2. 然后服务器将一个会话 token 发送给你的前端移动或 Web 应用。
    1. 这个 token 然后存储在应用的 cookie 或本地存储中。
  3. 接下来,假设你写好并提交了一条推文。然后,你的应用会随着你的推文一起发送会话 token(通过 cookie 或 header),以便服务器能够识别你是谁。但 token 只是一个随机字符串,服务器怎么能仅通过会话 token 就知道你是谁呢? 
  4. 当服务器收到会话 token 时,它不知道用户是谁,所以会将 token 发送到数据库,以从该 token 中检索 (4a) 实际的用户信息(如 userId)。 
  5. 如果用户存在且被允许执行该操作(即发送推文),服务器就会允许他们执行该操作。
  6. 最后,它会通知前端推文已发送成功。
Grey 2

传统方法的主要问题

问题在于第四步很慢,并且用户执行的每一个操作都需要重复这一步骤。因此,每一次 API 调用都会导致至少两次缓慢的数据库调用,这会降低整体响应时间。 

有两种方法可以解决这个问题

  1. 完全消除用户数据库查询(即消除第四步)。 
  2. 让额外的数据库查询快得多,这样额外的跳跃就不会有问题了。 

选项 1:消除数据库查询(第四步)

有不同的方法可以实现这一点。 

  1. 你可以将状态存储在服务器的内存中。但这在扩展时会产生问题,因为此状态仅在特定服务器上可用。 
  2. 使用“粘性会话”。在这里,你告诉负载均衡器即使在扩展后也始终将流量导向特定的服务器。这也会导致不同的扩展问题,如果服务器宕机(缩减),你将丢失所有状态。
  3. 第三种替代方案是 JWT。我们接下来会看看这个。

现在来看看使用 JWT 的方法。

JWT 方法

JWT,特别是当用作会话时,试图通过完全消除数据库查询来解决问题。 

主要思想是将用户信息存储在会话 token 本身中!因此,不是一个长的随机字符串,而是将实际用户信息存储在会话 token 本身中。为了安全起见,使用只有服务器知道的密钥对 token 的一部分进行签名。

因此,即使客户端和服务器可以看到 token 的用户信息部分,第二部分,即签名部分,只能由服务器验证。 

在下图中,token 的粉色部分包含有效负载(用户信息),客户端或服务器都可以看到。 

但蓝色部分是使用一个密钥字符串、头部信息以及有效负载本身进行签名的。因此,如果客户端篡改了有效负载(例如冒充不同的用户),签名就会不同,并且无法通过身份验证。

Encoded

以下是使用 JWT 时的用例情景

  1. 你使用用户名和密码登录
    1. 服务器通过查询数据库验证用户身份
    2. 然后服务器使用用户信息和密钥创建一个 JWT 会话 token(不涉及数据库)
  2. 然后服务器将一个 JWT token 发送给你的前端应用。对于未来的活动,用户只需发送 JWT token 来识别用户,而无需每次都登录。
    1. 一个 JWT token 看起来像这样:<头部>.<有效负载>.<签名>
  3. 接下来,假设你写好并提交了一条推文。当你发送它时,你的应用会随着推文的文本一起发送 JWT token(通过 cookie 或 header),以便服务器能够识别你是谁。但是服务器如何仅仅通过 JWT token 就知道你是谁呢?嗯,token 的一部分已经包含了用户信息。
  4. 因此,当服务器收到 JWT token 时,它使用密钥来验证签名部分,并从有效负载部分获取用户信息。从而消除了数据库调用。
  5. 如果签名验证通过,就允许他们执行该操作。
  6. 最后向前端发送推文已保存的通知

今后,对于用户的每一个操作,服务器只需验证签名部分,获取用户信息,然后让用户执行该操作。从而完全跳过了数据库调用。

Grey 1

token 过期

但关于 JWT token 还有一件额外且重要的事情需要了解。那就是它使用一个过期时间来使自己过期。通常设置为 5 分钟到 30 分钟。而且由于它是自包含的,你无法轻松地撤销/使其失效/更新它。这才是问题的症结所在。

那么为什么 JWT 对用户身份验证来说是危险的呢?

JWT 最大的问题是 token 撤销问题。由于它会一直有效直到过期,服务器没有简单的方法来撤销它。

以下是一些可能使这变得危险的用例。

  1. 退出登录并不能真正让你退出!

想象一下你在发推后退出了 Twitter。你可能会认为你已经退出了服务器,但事实并非如此。因为 JWT 是自包含的,它会一直工作直到过期。这可能是 5 分钟或 30 分钟,或者 token 中设置的任何时长。因此,如果有人在这段时间内获取了该 token,他们可以继续访问,直到它过期。

  1. 阻止用户并不能立即生效。

想象一下,你是 Twitter 或某个在线实时游戏的版主,该系统中有很多真实用户。作为版主,你想快速阻止某人滥用系统。你做不到,原因还是一样。即使你阻止了该用户,他们仍将继续访问服务器,直到 token 过期。

  1. 可能包含过期数据

想象一下,用户是管理员,但权限被降级为普通用户。这同样不会立即生效,用户将继续是管理员,直到 token 过期。

  1. JWT 通常不加密,因此任何能够进行中间人攻击并嗅探到 JWT 的人现在就拥有了你的认证凭据。这更容易实现,因为 MITM 攻击只需要在服务器和客户端之间的连接上完成。 

其他复杂性和注意事项

库和规范问题

已经发现许多实现 JWT 的库多年来存在许多安全问题,甚至规范本身也存在安全问题。即使是推广 JWT 的 Auth0 自己,也也曾遇到过问题

token 的长度

在许多复杂的实际应用中,你可能需要存储大量不同的信息。将其存储在 JWT token 中可能会超出允许的 URL 长度或 cookie 长度,从而导致问题。此外,你现在可能在每次请求中发送大量数据。

无论如何都需要维护状态(用于限速、IP 白名单等)

在许多实际应用中,服务器必须维护用户的 IP 并跟踪 API 以进行限速和 IP 白名单。因此,无论如何你都需要使用一个速度极快的数据库。认为你的应用可以通过 JWT 变成无状态的,这根本不现实。 

建议的替代方案

一个流行的解决方案是将一个“已撤销 token”列表存储在数据库中,并在每次调用时进行检查。如果 token 在已撤销列表中,则阻止用户执行下一步操作。但这样一来,你又需要额外调用数据库来检查 token 是否已撤销,从而完全违背了使用 JWT 的初衷。 

总结

虽然 JWT 确实消除了数据库查询,但它在此过程中引入了安全问题和其他复杂性。安全性是二元的——要么安全,要么不安全。因此,将 JWT 用于用户会话是危险的。

我可以在哪里使用它?

在一些场景中,你在后端进行服务器到服务器(或微服务到微服务)通信,一个服务可以生成一个 JWT token 发送给另一个服务用于授权目的。还有一些其他狭窄的场景,比如重置密码,你可以在其中发送一个 JWT token 作为一次性、短时有效的 token 来验证用户的电子邮件。

如果不能使用 JWT,我还能做什么?

解决方案是完全不将 JWT 用于会话目的。而是以更高效的方式采用传统的、但经过实战检验的方法。

也就是说,让数据库查询速度快得惊人(亚毫秒级),以至于额外的调用完全没有影响。

选项 2:让查询速度快得惊人,以至于无关紧要(一个经过实战检验的解决方案) 

有没有哪个数据库的速度快得惊人,可以在亚毫秒内处理数百万个请求? 

当然有。它叫做 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)。

请注意,闪电表情符号表示速度极快。蜗牛表情符号表示速度慢。

Session JWT

如前所述,你也可以将 Redis 用作整个数据层的主数据库。在这种情况下,你的架构将变得更加简单,基本上一切都会变得极快。

Db JWT

它能扩展吗?

当然,公司不仅将 Redis 用作独立的数据库,还用作地理分布式数据库集群。

[1] 参考资料

  1. 停止将 JWT 用于会话
  2. JWT 不应成为你会话的默认选项
  3. 为什么 JWT 不利于身份验证 – Randall Degges (Okta 开发者关系负责人)
  4. 停止将 JWT 用于会话,第 2 部分:为什么你的解决方案不起作用
  5. Thomas H. Ptacek 在 Hacker News 上的观点
  6. 我使用 JSON Web Token 的经验
  7. Web 上的身份验证(会话、Cookie、JWT、localStorage 等)
  8. Thomas H. Ptacek 的博客