不要用 JWT 来做 Web 应用的会话管理

13,919 阅读10分钟

时间被浪费在无谓的事情上。 —— m31271n

声明

本文内容的作用范围仅限 Web 应用。

预备知识

认证 / 授权

  • 认证(Authentication):验证目标对象身份。比如,通过用户名和密码登录某个系统就是认证。
  • 授权(Authorization):给予通过验证的目标对象操作权限。

更简单地说:

  • 认证解决了「你是谁」的问题。
  • 授权解决了「你能做什么」的问题。

通常,从目标用户的角度来观察,认证与授权会被简化为一步。

Web 应用中的存储机制与认证授权机制

很多人将 Cookie 与 JWT 做比较,这是完全错误的。Cookie 是一种数据存储机制,而 JWT 是一种认证授权机制,比较两者,毫无意义。

Web 应用中的两种数据存储机制:

  • Cookie
  • HTML5 Web Storage

Web 应用中的两种认证授权机制:

  • Session
  • JWT

词汇定义

为了避免行文过程中的含糊不清,先定义几个有关认证授权信息存储方式的词汇:

  • 无状态 JWT:JWT 中存储所有认证授权信息,服务端不存储任何相关数据。
  • 有状态 JWT:JWT 中存储认证授权信息的 ID,具体数据存储在服务端。
  • Session / Cookie :传统的 Session / Cookie 存储方式,有几种实现形式:
    • 签名的 Cookie 中直接存储 Session 信息,服务端不存储 Session 信息(与无状态 JWT 类似)
    • 签名的 Cookie 中存储 Session ID,服务端存储 Session 信息(与有状态 JWT 类似)

不签名的 Cookie 由于安全性过低,不推荐使用。所以这里也不再提及。

使用 JWT 的荒唐理由

很多人推荐在前后端分离的 Web 应用中使用 JWT 来做认证授权,他们的理由如下:

  • 易于横向拓展
  • 易于使用
  • 更灵活
  • 更安全
    • 避免了 CSRF 攻击
  • 内置过期功能
  • 移动设备上的兼容性更好,而不像 Cookie
  • 即使用户禁用 Cookie,应用也能很好的工作
  • RESTful API 应该是无状态的,所以认证授权也应该是无状态的
  • ……

不过,个人觉得这些都是些站不住的脚的理由。

易于横向拓展

这是从技术上讲,唯一一个说得过去的理由,但这也仅限于使用无状态的 JWT 的情境。

易于使用

这是最让我难以信服的理由,在 Web 应用中,JWT 的使用一点都不简单。

设想一个使用 JWT 进行认证授权的前后端分离应用:

  • 初期:通过用户名 / 密码进行认证授权,获取到可用的无状态 JWT。通过获取到的无状态 JWT 访问 API,好简单。
  • 需求变更 1:JWT 没有过期时间的话,被恶意获取并滥用,该怎么办?于是为无状态 JWT 加上了过期时间。
  • 需求变更 2:加上了过期时间的无状态 JWT,在用户将要提交表单的前一刻过期了,用户得重新登录,用户不能接受这种设定,表示不再使用你的应用。于是客户端开始使用各种策略刷新无状态 JWT。
  • 需求变更 3:用户说他的帐号密码泄漏了,有人登录了他的帐号。客服帮忙重置了密码,但是恶意登录者通过尚未过期的无状态 JWT 又改了密码,用户仍旧无法登录自己的帐号。所以,修改密码后,得让修改密码前的所有无状态 JWT 失效。然而,这是无状态 JWT 做不到的。于是,不得不开始在服务端存储一些 JWT 的相关信息,以获取对用户的更高级别的控制权。就这样,无状态 JWT 变成了有状态 JWT。
  • 后续:有状态 JWT 不如 Session / Cookie 历史悠久,可能还有前人没踩到的坑,叹气。

以上描述的使用 JWT 进行认证授权的前后端分离应用,客户端要处理 JWT 的刷新,服务端需要保存 JWT 的相关状态,也就是说,前后端都要去维护 JWT 的状态。而传统 Session / Cookie 的认证授权方式只需要后端维护状态。

所以,个人认为,Web 应用中使用 JWT 是让应用变得更复杂了。

驳回。

更灵活

「JWT 中可以随意地添加数据,这样可以更灵活。」但,差不多所有的 Session 实现也都能随意地保存数据。凭借这个理由来说 JWT 优于 Session / Cookie,难以令人信服。

驳回。

更安全

「JWT 做了密码学上加密,所以更安全。」其实,JWT 中只涉及到了 Base64 编码和签名,并没有涉及到加密,它只能保证数据不被篡改,而不能用来加密数据。从这点上来讲,JWT 中包含的数据和签名的 Cookie 中包含的数据的处境相同 —— 防篡改。

「浏览器请求资源时,总会携带 Cookie 中的相应的数据,这会有受到 CSRF 攻击的风险。而 JWT 则不会。」文章的开头提到:Cookie 是数据存储机制,JWT 是认证授权机制,这两者并不相关。命题有问题,就不反驳了。

错误问题的正确答案没有意义。

倒不如说「浏览器请求资源时,总会携带 Cookie 中的相应的数据,这会有受到 CSRF 攻击的风险。而如果把相关信息存到 HTML Web Storage 里,就不会有这个风险了。」确实,不会有受到 CSRF 攻击的风险了。但受 XSS 影响的可能性却增大了—— 浏览器并不能限制 JavaScript 环境中的 API 对 HTML5 Web Storage 的访问,其中保存的信息在 XSS 攻击面前变得唾手可得。

总结:

  • JWT 存储在开启 HttpOnlySecure 的 Cookie 里,这和 Cookie 里存储 Session 信息没什么差别。会受到 CSRF 攻击,但不会受 XSS 攻击。
  • JWT 存储在 HTML5 Web Storage 里,更不安全了。

使用 JWT 并没有更安全。

驳回。

内置过期功能

说实话,这个功能对于 Web 应用来说,没什么用。服务端的设置过期的能力才是更可靠的。如果依赖存储在客户端的 JWT 的过期功能,很多事情都是没法做或是需要付出更多成本的,比如:

  1. 重设密码后,强制用户登出,重新登录。如果不借助其他机制来限制 JWT,即使用户登出并重新登录。在原有的 JWT 过期以前,还是能通过原有 JWT 来访问 API 的。
  2. ……

驳回。

移动设备上的兼容性更好

移动设备上的主流浏览器、主流移动设备开发框架、严谨的 HTTP 库都有对 Cookie 的良好支持。

「移动设备上的兼容性更好」算是个理由,但是过时了。

驳回。

即使用户禁用 Cookie,应用也能很好的工作

如果用户禁用了 Cookie,通常情况下,他们也会禁用其他持久化存储机制,比如 HTML5 Web Storage。那,JWT 也无法工作了。

驳回。

知道为何禁用 Cookie 的用户,绝大多数都清楚这会影响他们的使用体验。开发者不应该为「用户禁用 Cookie」寻找折中的解决方案,而应该提示用户启用 Cookie,并告知用户为什么要使用 Cookie。

RESTful API 应该是无状态的,所以认证授权也应该是无状态的

用无状态 JWT 实现一个精确统计在线用户数量的功能,再来重新说这个话。

驳回。

JWT 的缺点

将以上的内容,总结总结,可以归纳出几个缺点。另外,再加上几个没提到的缺点。

占用太多空间

上面提到 JWT 不应该被存储在 HTML5 Web Storage 里,那就只能存储在 Cookie 里了。但是,Cookie 的存储容量是有限制的(通常为 4 KB)。

而,JWT 占用的空间又不是那么小,尤其是在使用无状态 JWT 时,所有的数据都被放到 JWT 里,数据大小很快就会超过 Cookie 的容量限制。

无法完全掌控其有效期。

从最初设计上讲,无状态 JWT 不支持撤销。不管发生什么,JWT 会持续有效,直到过期时间。

如果不通过带状态的 JWT 管理机制将 JWT 管理起来,是无法完全掌控其有效期的。

数据实效性差

无状态 JWT 一旦被生成,就不会再和服务端有任何瓜葛。

一旦服务端中的相关数据更新,无状态 JWT 中存储的数据由于得不到更新,就变成了过期的数据。

如果 JWT 中存储有权限相关信息,比如当前角色为 admin,但是由于 JWT 所有者滥用自身权利,高级管理员将权利滥用者的角色降为 user。但是由于 JWT 无法实时刷新,必需要等到 JWT 过期,重新登录时,高级管理员的设置才能生效。

实现缺乏真实场景的检验

以上这些缺点都只针对无状态 JWT。如果使用有状态 JWT,就可以规避这些缺点。

不过,有状态 JWT 基本上等同于 Session / Cookie。但,不同于 Session / Cookie,有状态 JWT 很少有经过真实场景检验的实现。

已存在的 Session / Cookie 实现(比如 express-session)已经在生产环境使用了很多很多年,因此,它很多安全方面的问题都得到了解决。但有状态 JWT 的实现却没有这方面的检验。

对 JWT 无意义的修补

别为了会话管理,费心费力修补 JWT 了,毫无意思。

对 JWT 无意义的修补对 JWT 无意义的修补

适合 JWT 的场景

JWT 的最佳用途是「一次性授权 Token」,这种场景下的 Token 的特性如下:

  • 有效期短
  • 只希望被使用一次

真实场景的例子 —— 文件托管服务,由两部分组成:

  • Web 应用:这是一个可以被用户登录并维持状态的应用,用户在应用中挑选想要下载的文件。
  • 文件下载服务:无状态下载服务,只允许通过密钥下载。

如何把 JWT 用在这个场景中呢?

  1. 用户登录到 Web 应用中,挑选好想要下载的文件,点击下载。
  2. 认证服务颁发包含下载信息的、具有较短过期时间的 JWT。JWT 中包含的信息可以是这样的:

    {  "file": "/books/我这一辈子.pdf",  "exp": 1500719759621}
  3. 使用 JWT 从文件下载服务下载文件。

如果觉得这个例子不贴切,就当我随意写了一个吧。

总结

  • 无状态 JWT
    • 不能被撤销或更新
    • 存储在 Cookie 中,会面临存储空间受限的问题
    • 存储在 HTML5 Web Storage 中,会面临 XSS 攻击的问题
  • 有状态 JWT
    • 与 Session / Cookie 类似,但缺乏真实场景的检验

不要用 JWT 来做 Web 应用的会话管理,请用 Session / Cookie。

参考

最后

才疏学浅,写不下去了。;]

使用 JWT 来作 Web 应用的会话管理并发控制的套路