停止使用 JWT 作为 Session

翻译自国外的文章

停止使用 JWT 作为 Session

最近,我看到越来越多的人推荐使用 JWT(JSON Web Tokens)来管理 web 程序中用户的 session。这是一个非常糟糕的主意,我将在这篇文章中解释原因。

为了防止一些歧义,我首先定义一些术语:

[[Web 中的 Session#Client-Side Session |无状态]] JWT:一个包含 session data 的 JWT 令牌,数据直接编码到令牌中。

[[Web 中的 Session#Server-Side Session |有状态]] JWT:一个只包含 session 引用或 ID 的 JWT 令牌,session data 存储在服务器端。

Session token / cookie:一个标准的(可选签名的)会话 ID,就像网络框架已经使用了很长时间,session data 存储在服务器端。

清楚地说:这篇文章并非不让你使用 JWT,只是说它不适合作为 Session 机制,使用它有风险。在其他领域,对它们确实存在有效的用途。在文章的最后,我将简单介绍。

前言

很多人错误地比较”cookies 与 JWT”,这种比较完全没有意义,好比拿苹果与橙子相比。cookies 是一种存储机制,而 JWT 是经过加密签名的 token。

它们并非对立关系,相反,它们可以一起使用也可以独立使用。正确的比较应该是 「Session 与 JWT」和「cookies 与本地存储」。

在这篇文章中,我将比较 session 和 JWT token,并在需要的地方涉及 「cookies 与本地存储」的比较。

大家谈论的 JWT 优势

当人们推荐 JWT 时,他们通常会跟你说如下优势:

  • 更易于(水平)扩展
  • 更易于使用
  • 更灵活
  • 更安全
  • 内置过期功能
  • 不需要用户 “Accept Cookie”
  • 防止跨站请求伪造(CSRF)
  • 在移动设备上使用效果好
  • 禁止 cookie 的场景很适用

我将对这些优势一一剖析解答,它们为什么是错误或误导性的。下面的一些解释可能有点含糊,主要是因为这些优势本身就很模糊。

更易于(水平)扩展

这是唯一一个从技术上说的过去优势,但只有在无状态 JWT token 的情况下才适用。然而实际上,几乎没有人真正需要这种扩展性。有许多更简单的扩展方法,除非你的操作范围达到 Reddit 的大小,你不需要 无状态会话

扩展有状态会话的一些例子:

  1. 当你在一个服务器上运行多个后台进程:在该服务器上使用 Redis 存储 Session 数据。
  2. 当你在多个服务器上运行:使用一个专门存储 Session 数据的 Redis 服务器
  3. 当你在多个服务器上,在多个集群中运行:粘连 Session

这些场景,现有软件都提供很好地支持,你开发的应用程序很大概率只能达到第二种情况。

也许你在想,你应该为你的应用程序以后做好准备,以防超越了那个界限。然而在实践中,切换 Session 机制是相当简单的,唯一的成本就是在你过渡的时候将每个用户注销一次,需要将所有用户一次性注销登录。从一开始就实施 JWT 并不值得,特别是考虑到我稍后将要讲到的缺点。

更易于使用

并非如此,你将不得不自己处理 session 管理,在客户端和服务器端都要进行,而标准的 session 和 cookies 搭配是开箱即用的。在任何方面,JWT 都不会更易用。

更灵活

我还没有看到有人真正解释 JWT 是如何更灵活的。几乎每一个主要的会话实现都允许你存储任意的 session data,这与 JWT 的工作方式没有任何区别。就我所知,这只是被当作一个热门词汇使用的。如果你有异议,随时可以向我提供例子。

更安全

很多人认为 JWT 令牌「更安全」,是因为使用了加密技术。虽然签名的 cookies 比未签名的 cookies 更安全,但这并不是 JWT 所独有的,好的 Session 实现也会使用签名的 cookies。

JWT 使用加密技术并不会神奇地使某个东西更安全,它必须有一个特定的目标,且对于那个特定的目标,它是一种有效的解决方案。事实上,错误的使用加密技术可能会变得更不安全。

另一个我经常听到的「更安全」的论点是「他们不是作为 cookie 发送出去的」。这完全没有道理,cookie 只是一个 HTTP 头,使用 cookies 并没有什么不安全。实际上,cookies 对防止恶意客户端代码特别有效,这是我稍后要讲的。

如果你担心有人截取你的会话 cookie,你应该使用 TLS,如果你不使用 TLS,任何类型的会话实现都会被截取,包括 JWT。

内置过期功能

这并没有什么意义的,也不是一个有用的特性。服务器端一样可以很好地实现过期功能,许很多实现方式就是如此。实际上,服务端过期是更好的选择,它允许应用程序清理不需要的 Session 数据,这是「有状态的 JWT token」过期机制是无法做到的。

这是完全错误的。没有所谓的 “cookie 策略”,关于 cookie 的各种规定实际上覆盖了任何并非严格必要的持久性标识符。你能想到的任何会话机制都包含在这其中。

总的来说:

如果你出于功能性目的(比如,让用户保持登录状态)正在使用 Session 或 JWT,那么无论你如何存储该会话,你都不需要向用户请求同意。 如果你出于其他目的(比如,分析或跟踪)正在使用会话或令牌,那么无论你如何存储该会话,你都需要向用户请求同意。

防止跨站请求伪造(CSRF)

事实并非如此。存储 JWT 大致有两种方式:

在 cookie 中:容易受到 CSRF 攻击,仍然需要防护。 在其他地方,比如 Local Storage:现在你不再容易受到 CSRF 攻击,但你的应用程序或网站现在需要 JavaScript 才能工作,并且你把自己暴露给了一个完全不同的,可能更糟糕的漏洞类型。下面有更多关于这点的内容。 唯一正确的 CSRF 缓解措施是使用 CSRF 令牌。这里的会话机制并不相关。

对移动设备上支持更好

无稽之谈。现有的每一款移动浏览器都支持 cookies,因此也支持会话。每个主要的移动开发框架和任何权威的 HTTP 库也是如此。所以这根本就不是问题。

对那些阻止 cookies 的用户也起作用

不太可能。用户不仅仅是阻止 cookies,他们通常会阻止所有持久化的方式。那包括 Local Storage 和任何其他可以让你持久化会话的存储机制(无论是否使用 JWT)。你是否使用 JWT 在这里根本不重要,这是一个完全不同的问题,试图在没有 cookies 的情况下让认证工作,基本上是徒劳的。

除此之外,那些阻止所有 cookies 的用户通常明白,这将会破坏他们的认证功能,他们会为他们关心的网站单独解锁 cookies。作为一个网络开发者,你无需解决这个问题;一个更好的解决方案是告诉你的用户为什么你的网站需要 cookies 才能工作。

JWT 缺点

既然我已经讨论了所有常见的观点和它们为何错误,你可能会想“哦,这没什么大不了的,即使 JWT 对我没有帮助,我仍然可以使用他”,但你错了。使用 JWT 作为会话机制确实有很多缺点,其中一些是严重的安全问题。

JWT 令牌空间占用较大

特别是当使用无状态的 JWT 令牌时,所有的数据都直接编码进令牌中,你将很快超过 cookie 或 URL 的大小限制。你可能决定将它们存储在 Local Storage 中,然而……

安全性更差

当你将 JWT 存储在 cookie 中时,它与任何其他会话标识符没有什么区别。但当你把你的 JWT 存储在其他地方时,你现在就容易受到新一类攻击的威胁,在这篇文章(特别是”存储会话”部分)中有所描述:

我们从上次停止的地方开始:回到本地存储 (Local Storage),这是一个伟大的 HTML5 新增功能,可以在浏览器和 cookie 中添加键/值存储。那么我们应该在本地存储中存储 JWT 吗?考虑到这些令牌可能达到的大小,这样做似乎有点道理。cookies 通常在大约 4k 的存储空间上达到上限。对于大型的令牌,cookie 可能不适合,本地存储会是显而易见的解决方案。然而,本地存储并没有提供 cookie 所提供的任何安全机制。

与 cookies 不同,本地存储并不会在每一个请求中发送你的数据存储内容。从本地存储中检索数据的唯一方式就是使用 JavaScript,这意味着任何经过内容安全策略检查的攻击者提供的 JavaScript 都可以访问和提取它。而且,JavaScript 也不关心或跟踪数据是否通过 HTTPS 发送。在 JavaScript 看来,这只是数据,浏览器会对其进行操作,就像对待其他任何数据一样。

Cookie 开发工程师为了保护 Cookie 内容做了巨大努力,而我们现在正尝试忽视他们给我们的技巧。在我看来,这似乎有点倒退。

简而言之,无论你是否使用 JWT,都必须使用 cookie。

无法使单个 JWT 令牌失效

还有更多的安全问题。与会话不同,会话可以在任何时候被服务器无效化,但是无状态的 JWT 令牌无法被单个无效化。设计上,它们将一直有效,直到它们过期,不论发生了什么。这意味着你不能,例如,在检测到攻击者后使攻击者的会话无效。当用户更改密码时,你也不能使旧的会话无效。

你基本上是无能为力的,你不能”杀死”一个会话,除非建立复杂的(并且有状态的!)基础设施来明确地检测和驳回它们,这已经违背使用无状态 JWT 令牌的全部意图。

数据过期

这个问题与上述问题有些相关,而且是另一个存在的安全问题。就像缓存一样,无状态令牌中的数据最终会「过期」,并非数据库中的最新数据。

这意味着一个令牌包含一些过时的信息,如某人在其个人资料中更改的旧网站 URL - 但更严重的是,这也可能意味着某人有一个具有管理员角色的令牌,尽管你刚刚撤销了他们的管理员角色。因为你也无法让令牌失效,自然无法撤回他们的管理员访问权限,除非关闭整个系统。

实现的经验不足或者根本不存在

你可能认为所有这些问题只与无状态 JWT 令牌有关,你可能大部分是对的。然而,使用有状态的令牌基本上等同于一个常规的 Session…但没有经过严格测试。

现有的会话实现(例如 Express 的 express-session)已在生产环境中运行了很多,很多年,因此它们的安全性得到了很大的提高。当你使用 JWT 令牌作为会话 cookie 的临时替代品时,你无法获得这些好处 - 你将不得不自行实现(并且在过程中很可能引入漏洞),或者使用一个没有经过实际检验的第三方实现。

结论

无状态的 JWT 令牌不能过期或更新,而且根据你存储它们的地方,会引入大小问题或安全性问题。有状态的 JWT 令牌在功能上与会话 cookie 相同,但没有经过严格测试和好的实现或客户端支持。

除非你在开发一个类似 Reddit 规模的应用,否则没有理由把 JWT 令牌作为会话机制。只使用 Session 就好。

那么……JWT 到底有什么用呢?

在这篇文章的开头,我说过 JWT 有其良好的应用场景,但它们作为会话机制是不合适的。这仍然是正确的;JWT 特别有效的使用场景通常是那些把它们作为 一次性授权令牌 的场景。

从 JSON Web Token 的规范中:

JSON Web Token (JWT)是一种紧凑的、URL 安全的,用于在两方之间传输待传输声明的方式。[…]这使得这些声明可以通过数字签名或使用消息认证码(MAC)的完整性保护和/或加密。

在这种情境下,”声明”可以被理解为”命令”,一次性授权,或者你可以将其描述为如下的任何其他场景:

你好,服务器 B,服务器 A 告诉我我可以<在此处填写声明>,这是这里的(加密的)证据。

例如,你可能在运营一个文件托管服务,用户需要验证身份才能下载他们的文件,但文件本身是由一个单独的、无状态的“下载服务器”提供的。在这种情况下,你可能希望你的应用程序服务器(服务器 A)发布单次使用的”下载令牌”,然后客户端可以使用这个令牌从下载服务器(服务器 B)下载文件。

以这种方式使用 JWT 时,有一些特定的属性:

  • 令牌的生命周期很短。它们只需要在几分钟内有效,以允许客户端开始下载。
  • 令牌只预计使用一次。应用服务器将对每次下载发出新的令牌,所以任何一个令牌只用于请求文件一次,然后就被丢弃。根本没有持久的状态。
  • 应用服务器仍然使用会话。只有下载服务器使用令牌来授权单个下载,因为它不需要持久的状态。

如你所见,在这里,将会话和 JWT 令牌结合起来是完全合理的 - 它们各自有自己的目的,有时你需要两者。只是不要使用 JWT 来处理持久的,生命周期长的数据。


停止使用 JWT 作为 Session
http://wszzf.top/2023/07/29/停止使用 JWT 作为 Session/
作者
Greek
发布于
2023年7月29日
许可协议