Cookies
在设计网络应用程序时,(尤其是传统的HTML类型),你将在某一时刻必须弄清楚如何登录用户并在两次请求之间保持他们的登录状态。
我们在这方面使用的核心机制是cookies。Cookies是由服务器发送至客户端的小字符串。客户端收到这个字符串后,会在随后的请求中重复这个字符串。我们可以在cookie中存储一个 “用户ID”,对于未来的任何请求,我们都会知道客户是什么用户。
但这是非常不安全的。这些信息存在于浏览器中,这意味着用户可以改变用户ID
,被识别为不同的用户。
Sessions
解决这个问题的传统方法是所谓的 “Sessions”。 我不知道会话最早的用法是什么,但它存在于每一个网络框架中,而且自从网络框架出现以来就一直存在。
通常情况下,Sessions 和 Cookie 被描述为两种不同的东西,但它们实际上不是。会话需要一个 cookie 来工作。
Cookie: MY_SESSION_ID=WW91IGdvdCBtZS4gRE0gbWUgb24gdHdpdHRlciBmb3IgYSBmcmVlIGNvb2tpZQ
复制代码
我们不是向客户发送一个可预测的用户ID,而是向客户发送一个完全随机的 sessions ID,这是很难猜到的。这个ID没有进一步的意义,也不会解码成任何东西。这有时被称为一个不透明的标记。
当客户端向服务器重复这个 sessions ID时,服务器将在(例如)数据库中查找这个ID,从而将其与用户ID联系起来。 当用户想注销时,sessions ID将从数据存储中删除,这意味着cookie不再与用户相关联。
会话数据存储在哪里?
像PHP这样的语言有一个内置的存储系统,默认情况下会把数据存储在本地文件系统中。在Node.js生态系统中,默认情况下,这些数据将被放在 “内存 “中,并在服务器重新启动后消失。
这些方法在开发者机器上是有意义的,或者当网站被托管在长寿命的裸机服务器上时,但现在的部署通常意味着一个全新的 “系统”,所以这些信息需要被存储在一个比服务器寿命更长的地方。一个简单的选择是数据库,网站普遍使用Redis和Memcached这样的系统,这对微小的网站有效,也在大规模的情况下有效。
加密的 token
10多年前,我开始更多地使用OAuth v1和类似的认证系统,我想知道我们是否可以直接将所有的信息存储在cookie中,并对其进行加密签名。
尽管得到了一些很好的答案,但我还是没有去做,因为我觉得没有足够的信心来保证这个安全,而且我觉得这需要比我更了解加密技术。
几年后,我们有了JWT,而且它很火爆。JWT本身是一个加密/签署JSON对象的标准,它被大量用于身份验证。 我们实际上再次嵌入了user_id
,而不是在cookie中的一个不透明的 token,但我们包括一个签名。签名只能由服务器生成,它是用一个 “秘密 “和cookie中的实际数据计算出来的。
这意味着,如果数据被篡改(user_id
被改变),签名不再匹配。
那么,为什么这很有用呢?我对此的最佳答案是,不需要有一个 session 数据的系统,如Redis或数据库。所有的信息都包含在JWT中,这意味着你的基础设施在理论上更简单。你有可能在每个请求的基础上减少对数据存储的调用。
缺点
使用JWT有一些主要的缺点。
首先,它是一个复杂的标准,用户很容易弄错设置。如果设置错误,在最坏的情况下,这可能意味着 任何人都 可以生成有效的JWT并冒充其他人。这也不是一个初学者级别的问题,去年Auth0的一个产品就有这样的问题。
Auth0是(或曾经是?他们刚刚被收购)一个主要的安全产品供应商,并且讽刺地赞助了jwt.io网站。如果他们不安全,普通(开发者)公众还有什么机会?
然而,这个问题是许多安全专家不喜欢JWT的更大原因的一部分:它有大量的功能和非常大的范围,这使它有很大的表面积,可以让库的作者或这些库的用户犯错误。有替代JWT的无状态 token ,其中一些确实解决了这个问题)。
第二个问题是 “注销”。在传统的会话中,你只需从会话存储中删除会话标记,这就足以让会话 “失效 “了。
对于JWT和其他无状态 token,这是不可能的。我们不能删除 token,因为它是自成一体的,没有中央机构可以使它们失效。
这通常是通过三种方式解决的。
- token 的寿命很短。例如,5分钟。在5分钟结束之前,我们生成一个新的。通常使用一个单独的刷新 token)。
- 维护一个拥有最近过期 token 列表的系统。
- 没有服务器驱动的注销,假设客户可以删除他们自己的 token。
好的系统通常会使用前两者。需要指出的是,为了支持注销,你可能仍然需要一个集中的存储机制(用于刷新 token、撤销列表或两者),这正是JWT应该 “解决 “的事情。
题外话:有些人喜欢JWT,因为每次请求所涉及的系统较少,但这与能够在过期前撤销 token 的做法相矛盾。
我最喜欢的解决方案是保留一个全局的JWTs列表,这些JWTs在过期前已经被撤销了(过期后删除这些 token)。与其让webservers点击服务器来获取这个列表,不如使用pub/sub机制将列表推送到每个服务器。
撤销 token 对安全很重要,但很少。现实上,这个列表很小,很容易装入内存。这在很大程度上解决了注销的问题。
JWT的最后一个问题是,它们相对较大,当在cookie中使用时,会增加很多每次请求的开销。
总而言之,仅仅是为了避免中央会话存储,这就有很多缺点。我不认为JWT是一个普遍的坏主意或没有好处,但有很多东西需要考虑。
为什么它们会流行?
在阅读科技博客时,有一件事让我很惊讶,那就是围绕JWT有 很多 的讨论。特别是在Medium和像/r/node这样的subreddits上,我极其经常地看到关于JWT的介绍。
我意识到这并不意味着 “JWT比session tokens更受欢迎”,就像 GraphQL 不比 REST 更受欢迎,NoSQL 不比关系型数据库更受欢迎一样:写那些已经被试用了十年以上的技术并不那么有趣(见:对新颖性的呼吁)。此外,写新的解决方案的主题专家很可能与自己的大多数读者有不同的问题和规模。
然而,这些新技术比简单的技术创造了更多的嗡嗡声,如果有足够多的人一直在谈论这个热门的东西,最终这可以转化为实际的采用,尽管它对于大多数简单的用例来说是次优的选择。
这类似于许多新的开发者在服务器渲染的 HTML 之前学习如何用 React 构建 SPA。有经验的开发者可能会觉得,服务器渲染的HTML可能应该是你的默认选择, 在需要的时候 建立一个 SPA,但这并不是新的开发者通常被教导的。
在考虑简单的选择之前就采用复杂的系统,这是我看到的更多的事情,但这让我对 JWT 感到惊讶。
作为一个练习,我查找了 /r/node 上提到 JWT 的最受欢迎的帖子(按票数)。我本来想看一下前100条,但看完前12条就觉得无聊了。
从这12篇文章和Github repos中。
- 1篇提到了使用撤销列表,3篇提到了refesh tokens。 剩下的文章和Github repositories只是没有注销的手段。
- 1篇文章提到,使用标准会话存储 可能会 更好。
- 1篇文章 同时 使用了标准会话存储和 JWT,使得 JWT 成为不需要的。
- 1篇 github 资源库带有预生成的私钥。(yup)
- 大多数文章使用的是几周或几个月的过期时间,3篇文章从未过期他们的 JWT。
除了1个,这些高票数的帖子质量极低,作者很可能没有资格写这个,有可能会造成现实世界的伤害。
所有这些至少证实了我的偏见,即安全 token 的 JWT 是很难搞好的。
关于JWT和规模
通过大量的 Reddit 帖子和评论,也让我对人们为什么认为 JWT 更好有了更精细的了解。各地的首要原因是:”它的可扩展性更强”,但人们 认为 在什么规模下会开始出现问题并不明显。我相信,问题开始出现的时间点可能比人们假设的要高得多。
我们大多数人都不是 Facebook,但即使在 “数百万活跃会话 “的情况下,分布式的 key -> value 存储也不可能崩溃。
据统计,我们中的大多数人都在构建树莓派都能轻松应对的应用程序。
结论
使用 JWTs 作为 token 增加了一些整洁的属性,并在某些情况下使你的服务有可能是无状态的,这在某些架构中可能是理想的属性。
采用 JWT 也有缺点。你要么放弃撤销,要么你需要建立基础设施,这比简单地采用会话存储和不透明的 token 要复杂得多。
我说这些并不是要阻止 JWT 的使用,而是在使用时要慎之又慎。要意识到安全和功能的权衡和陷阱。不要把它放在你的 “模板 “中,也不要把它作为默认选择。
鸣谢
感谢Nick Chang-Fong和Dominik Zogg为本文提供的反馈和建议。