现在,JSON Web Tokens (JWT) 是非常流行的。尤其是 Web 开发领域。
- 流行
- 安全
- 稳定
- 易用
- 支持 JSON
所有这些因素,令 JWT 名声大振。
但是,今天我要来说说使用 JWT 的缺点。也就是为什么说将 JWT 用于会话控制是多么的糟糕。
为什么使用 JWT?
如果你不了解 JWT,不要紧张,它并不可怕。
JWT 只是用于网络间传递声明而执行一种基于 JSON 的标准。
例如,我是个盲人,而且听力也不好。你上周帮我买了午餐,现在我需要你的收款账号,把钱还给你。如果我询问你的账号,但是其他人高呼他们的账号,由于我把别人的账号误认为是你的,我可能会不小心把钱打给别人。
JWT 旨在防止这种情况发生。JWT 提供了一种简单的方法,在彼此传递数据时,验证是由谁先创建了数据。
所以,像上述的例子,即使我收到了超过 100 万个 JWT 返回的账号信息,我也很容易可以辨别出来哪个真实来自于你。
JWT 如何运行?
JWT 是 JSON 格式的被加密了的字符串。
JWT 的核心是密钥,就是 JSON 数据。这是你关心的,并希望安全传递出去的数据。JWT 如何做到这一点,并使你信任它,就是加密签名。
比如说,我写了一封信,当我署名这封信时,意味着只要读过这封信的人,都知道是我写了这封信。而且我的签名是独一无二的,所以不会被怀疑真实性。加密签名的方式大致相同,JWT 有两种加密方式:对称加密和非对称加密,两种方式有同等的效用。
JWT 内容加密
其实,JWT 的内容(内部的 JSON 数据)通常是不加密的。这意味着,即使没有密钥,也可以查看 JWT 内的数据。JWT 默认并不会加密你的数据,它只是帮助你验证是你信任的一方创建了它。
如果你确实需要加密 JWT 内容,可以使用 JWE 进行加密,但这种做法并不常见。请确保使用了正确的方法。
如今人们是如何使用 JWT 的?
JWT 最常见的用途是身份验证。有大量的 Web 安全库使用 JWT 创建会话控制,API 令牌等。
这种做法通常是,当需要对网站/ API 进行身份验证时,服务器生成一个包含用户 ID 的 JWT,以及其它一些关键性的信息,然后再将其发送给浏览器/客户端等,存储为会话令牌。
例如,当用户访问网站上的另一个页面时,浏览器会自动将该 JWT 发送到服务器,服务器验证 JWT 确认和最初创建的令牌相同,然后允许用户执行后续的操作。
从理论上看,还不错,因为:
- 当服务器收到 JWT 时,可以验证其是否是合法的,是否是信任用户的令牌
- 可以在服务器本地验证,而不需要任何其它的网络请求,与数据库的通信等。这可能令管理会话更高效,因为无需从数据库(缓存)加载用户信息,只需要在本地运行一小部分代码。这可能是人们喜欢用 JWT 的最大原因。
似乎很棒,既可以提高 Web 应用的性能,又可以减少缓存服务器和数据库服务器的负载,提供更好的体验。另外你还可以在 JWT 中存储用户权限信息、用户个人信息等等更多的额外信息进一步减少数据库压力。
为什么 JWT 不是最好的会话令牌?
我们已经了解了 JWT 如何用于身份验证,让我们进入本篇的中心话题:为什么 JWT 不是最好的会话令牌,为什么普通的旧会话方式在几乎各方面都优于 JWT。
背景
我们先了解一些背景知识。开发人员构建的大多数网站都相对比较简单:
- 用户注册
- 用户登录
- 用户点击执行操作
- 网站使用用户信息进行创建、删除、更新、查阅信息
对于这类网站,要知道用户进行交互的每个页面都会包含某些动态数据。比如:你正在访问一个需要用户登录才能进一步操作的网站,你经常会在数据库中对用户进行这些操作:
- 记录用户正在执行的操作
- 将用户的某些信息添加到数据库中
- 检查用户的权限看其是否可以执行某项操作
- 等等
数据
我们来看两种方案:
- 在 Cookie 中存储用户 ID(abc123)
- 在 JWT 中存储用户 ID(abc123)
如果我们将 ID 存储在 Cookie 中,需要 6 个字节。如果将 ID 存储在 JWT 中(设置基本的请求头字段,以及一些其它信息),需要几百字节甚至更多。对于简单的会话控制,每个页面的请求就增大了几十倍。
假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多。
无论如何你需要操作数据库
如上所述,大多数需要用户登录的网站主要是 CRUD 操作(增查改删)生成动态内容。
在网站上使用 JWT,对于用户加载的几乎所有页面,都需要从缓存/数据库中加载用户信息,可能出现下列情况:
- 需要用户关键性信息查询(例如:判断用户账号是否有足够的资金完成交易?)
- 需要将一些信息保存进数据库(例如:用户相关的唯一信息,需要根据该信息对用户进行检索)
- 必须从缓存/数据库中查询完整的信息,方便网站生成完整的动态页面内容。
想想你的网站是否会遇到上述情形。这意味着大多数网站不适用 JWT 的无状态特性。为了解决这个问题,就需要 JWT 变得更大,而且需要使用 CPU 来计算签名,就会导致比传统会话慢许多。
其实,几乎每个 Web 框架支持在每次请求传入用户信息,这包括 Django,Rails,Express.js 等(如果有用到身份验证功能)。另外,如果你使用 Memcached/Redis 等缓存服务器对用户信息进行缓存,检索会变得非常快。
多余的签名
JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。
但是,其实在过去 20 年中几乎每一个框架对于普通会话 Cookie 都可以获得很好的加密签名处理。这意味着你可以获得与 JWT 完全一致的效果,况且大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。
听着似乎很赞,但是没有任何优势,为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。
更好的解决方案是什么?
如果你正在构建上述类型的网站,那么最好选择旧的,简单且安全的服务器端会话。而不是将用户 ID 存储到 JWT 中,然后再将 JWT 存储到 Cooike 中。只需将用户 ID 直接存储到 Cookie 中即可。
如果你的网站很受欢迎,有着大的访问量,可以将会话缓存到 Memcached/Redis,同时也有利于扩展你的服务。
什么时候使用 JWT?
JWT 虽然对于大多数网站都没有用,但是有几种情况它是很有用的。
如果你正在构建从服务器到服务器或客户端到服务器(如:移动应用 APP 或单页面应用)的 API 服务,那么使用 JWT 是非常明智的。比如:
- 你的客户端需要通过 API 进行身份验证,并返回 JWT
- 然后,客户端使用返回的 JWT 经过身份验证去请求其它的 API 服务
- 这些其它 API 服务通过客户端的 JWT 验证客户端是可信的,并且可以执行某些操作无需再次验证
对于这类 API 服务,JWT 非常适合,因为客户端需要频繁进行请求,并且权限是可控的,通常认证数据以无状态方式持久存在,不需要过多依赖用户信息。
如果你正在构建的应用类似单点登录或 OpenID Connect 认证,JWT 同样十分适合,就是实现一种通过第三方验证用户的方法。
总结
当你准备构建下一个网站时,只需要使用 Web 框架默认的身份认证功能即可,不需要再集成 JWT 方式。
关注公众号「展白说」,获取更多有价值的信息。