别再使用JWT做会话管理了(一)
不幸的是,最近我看到越来越多的人建议使用JWT(JSON Web Tokens)来管理其Web应用程序中的用户会话。这是一个糟糕的想法,在这篇文章中,我将解释原因。
为了防止任何混淆,我将首先定义一些术语:
-
无状态JWT:包含会话数据的JWT令牌,直接编码到令牌中。
-
有状态JWT:仅包含会话的引用或ID的JWT令牌。会话数据存储在服务器端。
-
会话令牌/ cookie:标准(可选签名)会话ID,就像Web框架已经使用了很长时间的形式。会话数据存储在服务器端。
要明确:本文并不认为你永远不应该使用JWT - 只是它不适合作为会话机制,并且这样使用它是危险的。在其他领域,确实存在有效的使用场景。在本文的最后,我将简要介绍其他用例。
前提注意
很多人错误地将“cookies与JWT”进行比较。这种比较完全没有意义,它就像将苹果与橙子进行比较。 cookie是一种存储机制,而JWT令牌是加密签名的令牌。
它们不是对立的 - 相反,它们可以一起使用或独立使用。正确的比较是“ session与JWT”和“cookie与local storage”。
在这篇文章中,我将把会话与JWT令牌进行比较,偶尔也会阐述“cookies vs. Local Storage”,这样做才有意义。
声称JWT的优势
当人们推荐JWT时,他们通常会声称以下一项或多项好处:
-
更容易(水平)缩放
-
更容易使用
-
更灵活
-
更安全
-
内置过期功能
-
无需向用户询问“cookie同意”
-
防止CSRF
-
在移动设备上更好用
-
适用于阻止cookie的用户
我会分别阐述这些主张中的每一个,以及为什么它们是错误的或误导性的。下面的一些解释可能有点含糊不清; 这主要是因为声称的优势本身也是含糊不清的。我很乐意更新它以给出更具体的说法; 您可以在本文底部找到我的联系方式。
更容易(水平)缩放
这是列表中唯一在技术层面上有些正确的声明,但仅限于使用无状态 JWT令牌。然而,现实是,几乎没有人真正需要这种可扩展性。有许多更简单的扩展方法,除非你的操作规模像Reddit那么大,否则你不需要'无状态会话'。
扩展有状态会话的一些示例:
-
在一个服务器上运行多个后端进程:(在该服务器上)用一个Redis守护进程进行Session存储。
-
在多个服务器上运行:使用一个专用服务器运行Redis进行Session存储。
-
在多个服务器上运行,在多个集群中运行:粘性(Sticky )Session。
这些都是现有软件良好支持的场景。您的应用程序是很有可能永远不会超越第二种情况。
也许你会想,你应该开发“面向未来”的应用程序,如果你也曾经扩展超出。然而,在实践中,后续更换会话机制是相当简单的。当您进行转换时,唯一的成本是每个用户注销一次。预先实施使用JWT是不值得的,特别是考虑到我后来会遇到的缺点。
更容易使用
这点真的不是。您必须自己在客户端和服务器端处理会话管理,而标准会话cookie 只能在开箱即用的情况下工作。JWT在任何方面都不容易。
更灵活
我还没有看到有人解释JWT 如何更灵活。几乎每一个主要的Session的实现都使您可以存储任意会话数据,这和JWT的工作方式是没有区别的。据我所知,JWT只是一个流行语。如果您不同意,请随时与我联系并附带示例。
更安全
很多人认为JWT令牌“更安全”,因为他们使用加密技术。虽然带签名的cookie比未签名的cookie更安全,但这并不是JWT所独有的。好的Session实现同样也在使用签名cookie。
使用加密也并不会神奇地使某些东西更加安全。它必须服务于特定目的,并且是针对该特定目的的有效解决方案。事实上,错误使用的加密技术可能会降低安全性。
我听到的“更安全”论点的另一个解释是“它们不是作为cookie发送的”。这绝对没有意义 - 一个cookie只是一个HTTP头,并且没有什么不安全的使用cookie。实际上,cookie 对于像恶意的客户端代码此类,是极其受到良好保护的。我将在稍后介绍。
如果您担心有人拦截您的会话cookie,您应该只使用TLS - 如果您不使用TLS,任何类型的会话实现都是可以截取的,包括JWT。
内置过期功能
这是无稽之谈,而不是一个有用的功能。到期也可以在服务器端实现,许多实现都可以。事实上,服务器端到期是可取的 - 它允许您的应用程序清理它不再需要的会话数据,如果您使用有状态JWT令牌并依赖其过期机制,则无法执行此操作。
无需向用户询问“cookie同意”
完全错了。没有“cookie法律”这样的东西 - 关于cookie的各种法律实际上涵盖了任何类型的持久标识符,这些标识符对于服务的运行并非严格必需。您可以想到的任何会话机制都将由此涵盖。
简而言之:
-
如果您使用的是Session或者Token用于功能性目的(例如,保持用户登录),那么无论你使用什么形式存储管理会话,你都不需要询问用户同意。
-
如果您使用的是会话令牌或用于其他目的(如分析或跟踪),不管你如何存储会话,那么你都需要询问用户同意。
防止CSRF
事实并非如此。存储JWT的方法大致有两种:
-
在cookie中:您仍然容易受到CSRF攻击,并且仍然需要对其进行保护。
-
在其他地方,如本地存储(Local Storage):您不容易受到CSRF攻击,但您的应用程序或站点需要使用JavaScript去运行,这使得自己容易受到完全不同的,可能更糟糕的漏洞类型的攻击。下文有更多关于此的内容。
唯一正确的CSRF缓解方式是CSRF令牌。会话机制与此无关。
在移动设备上更好用
目前在使用的每个移动浏览器都支持cookie,因此也支持Session。每个主要的移动开发框架和任何重要的HTTP库都是如此。这根本不是一个问题。
适用于阻止cookie的用户
不太可能。用户不仅可以阻止cookie,还可以阻止所有持久性方法。这包括本地存储,以及允许您持久保存会话的任何其他存储机制(使用或不使用JWT)。使不使用JWT这里并不重要,这是一个完全独立的问题。 让身份验证在没有cookie的情况下工作是一个失败的原因。
最重要的是,阻止所有 cookie的用户通常可以理解为这将破坏他们的身份验证功能,并为他们关心的网站单独解锁cookie。作为Web开发人员,这个问题不是您需要去解决的; 一个更好的解决方案是向您的用户解释您的网站为何需要使用Cookie。
缺点
现在,上文已经涵盖所有声称的特性,以及为什么他们是错误的,你可能会想:“哦,这不是什么大不了的,没有关系,即使它对我并没有没什么帮助,我仍然用JWT。你错了。使用JWT作为会话机制有很多缺点,其中一些是严重的安全问题。
他们占用更多空间
JWT Tokens并不是很小。特别是在使用无状态JWT令牌时,所有数据都直接编码到令牌中,您将很快超过cookie或URL的大小限制。您可能打算将它们存储在本地存储中 - 但是......
它们不太安全
将JWT存储在cookie中时,它与任何其他Session标识符没有区别。但是当你将JWT存储在其他地方时,你很容易受到本文所述的一类新攻击(特别是“存储会话”部分)的攻击:
回到本地存储,这是一个非常棒的HTML5添加,它为浏览器和cookie添加了一个键/值存储。那么我们应该将JWT存储在本地存储中吗?考虑到这些Tokens可以达到的大小,这可能是有意义的。Cookie通常高出大约4k的存储空间。对于大型令牌,cookie可能是不可能的,本地存储将是明显的解决方案。但是,本地存储不提供cookie所执行的任何相同安全机制。
与cookie不同,本地存储不会随每个请求发送数据存储的内容。从本地存储中检索数据的唯一方法是使用JavaScript,这意味着任何提供通过内容安全策略的JavaScript的攻击者都可以访问和泄露它。不仅如此,JavaScript还不关心或跟踪数据是否通过HTTPS发送。对于JavaScript而言,存储的Tokens只是数据,浏览器将像其他任何数据一样对其进行操作。
简而言之,使用cookie并不是可选的,无论您是否使用JWT 。
您不能使单个JWT令牌无效
还有更多的安全问题。与会话不同 - 无论何时感觉都可以由服务器无效 - 个别无状态JWT令牌不能被无效。根据设计,无论发生什么情况,它们都将有效,直到它们到期为止。这意味着,在检测到危害后,您无法使攻击者的会话无效。当用户更改密码时,您也无法使旧会话无效。
你本质上是无能为力的,如果没有构建复杂的(有状态的!)基础设施来明确地检测和拒绝它们,就无法“杀死”会话,从而打败了使用无状态JWT令牌。
数据变得陈旧
与此问题有些相关,还有另一个潜在的安全问题。就像在缓存中一样,无状态令牌中的数据最终会“过期”,并且不再反映数据库中最新版本的数据。
这可能意味着,一个令牌包含像一个老的网站URL一些过时的信息,有人在他们的个人资料改变。更严重的是,它也意味着某人有一个角色的令牌admin
,即使你只是吊销了其admin
作用。因为你不能令牌无效,你没有办法为你免除他们的管理员权限,除非短暂关闭整个系统。
实现缺乏实战或者根本不存在
您可能会认为所有这些问题都与无状态JWT令牌有关,并且您大多数都是正确的。但是,使用有状态令牌基本上相当于常规会话cookie ...但没有经过实战考验的实现。
现有的Session实现(例如express-session
Express)已经在生产中运行了许多年,并且由于这个原因,它们的安全性得到了很大的改进。使用JWT令牌作为临时会话cookie时,您不会获得这些好处。您将不得不推出自己的实现(并且很可能在此过程中引入漏洞),或者使用不常见的第三方实现。
结论
无状态JWT令牌不能无效或更新,并且会根据您存储它们的位置引入大小问题或安全问题。有状态的JWT令牌在功能上与会话cookie相同,但没有经过实战考验和经过良好审核的实现或客户端支持。
除非您使用Reddit规模的应用程序,否则没有理由将JWT令牌用作会话机制,只需使用Session即可。
那么...... JWT的优点是什么呢?
在本文开头,我说JWT 有很好的使用场景,但它们不适合作为会话机制。JWT特别有效的用例通常是被用作用户一次性授权令牌。
JSON Web令牌(JWT)是一种紧凑的,URL安全的方式,用于表示在双方之间转移的声明。[...]使声明能够通过消息验证码(Message Authentication Code)进行数字签名或完整性保护和/或加密。
在这种情况下,“声明”可以是类似“命令”,一次性授权或基本上任何其他可以表达的场景:
你好服务器B,服务器A告诉我,我可以<声称在这里>,这里是(加密)证明。
例如,您可能运行文件托管服务,用户必须在其中进行身份验证才能下载文件,但文件本身由单独的无状态“下载服务器”提供服务。在这种情况下,您可能希望让您的应用程序服务器(服务器A)发出一次性“下载令牌(Token)”,然后客户端可以使用该令牌Token文件从下载服务器(服务器B)下载该文件。
以这种方式使用JWT时,有一些特定的属性:
-
令牌是短暂的。它们只需要有效几分钟,以允许客户端启动下载。
-
该令牌只能使用一次。应用程序服务器会为每次下载发出一个新令牌,因此任何一个令牌只用于请求一次文件,然后被丢弃。有没有持久化状态。
-
应用程序服务器仍使用会话。只是下载服务器使用令牌来授权单个下载,因为它不需要持久状态。
正如你在这里看到的,结合Session和JWT Token是完全合理的。它们各有各的目的,有时你需要两者。不要给需要持久化、长期保存的数据使用JWT。
声明:本文翻译自《Stop using JWT for sessions》,已征得原文作者同意。如有翻译不正确或不合理部分,欢迎指出。