Stop using JWT for sessions
原文见 joepie91的博客: Stop using JWT for sessions(WTFPL 协议许可)
Update - June 19, 2016: A lot of people have been suggesting the same “solutions” to the problems below, but none of them are practical. I’ve published a new post with a slightly sarcastic flowchart - please have a look at it before suggesting a solution.
Stop using JWT for sessions, part 2: Why your solution doesn’t work
真是不幸,我最近看见越来越多的人建议使用 JWT(JSON Web Token)管理应用的会话。这是一个非常、非常糟糕的想法,在这篇博客里,我会解释下我为什么这么说。
为了避免读者困惑,我先定义一些术语:
- 无状态 JWT: JWT 令牌中包含全部的会话信息
- 有状态 JWT: JWT 令牌中只包含存储在会话 ID,会话信息存储在服务端
- Session 令牌/cookie: 标准会话 ID,会话存储在服务端
事先讲明,我写这篇文章并不是说:你一定不能用 JWT,只是 JWT 不适合做会话管理,而且,这样做也很危险。不过,JWT 在其它领域很有用。在这篇文章的结尾处,我会简单地介绍下。
题外话
很多人错误地 拿“Cookie 和 JWT”作比较。这种比较其实一点意义都没有,因为它们本就是风马流不相及的两种事物:Cookie 是存储机制,而 JWT 是加密签名令牌。
它们并不是对立的,你可以结合使用或者独立地使用它们。真正应该比较的是“Session 和 JWT”以及“Cookie 和本地存储”。
在这篇文章里,我主要对比 Session 和 JWT 令牌,也会偶尔提及 Cookie 和本地存储的对比,因为有必要这么做。
所谓的 JWT 优点
当人们推荐使用 JWT 时,他们往往会说 JWT 有如下(或更多)优点:
- 易(水平)拓展
- 易用
- 更灵活
- 更安全
- 内置过期功能
- 不需要用户同意使用 Cookie
- 预防 CSRF
- 更利于移动端使用
- 适用于禁用 Cookie 的用户
我会逐一对上述优点进行说明,证明它们为什么是错误的或者误导人。下面的说明可能有些比较含糊,主要是因为上述优点别人描述地也比较含糊。我乐于把说明写得更加具体,你们可以在文章末尾找到我的联系方式。
易(水平)扩展
这是上述列表中唯一一个有点正确的观点,不过也仅限于使用无状态 JWT 令牌时。事实是,几乎没人真正需要这种拓展性,因为有多种拓展方式。除非像 Reddit 那种网站,你不会需要无状态的会话。
以下是有状态会话的几种拓展方式:
在一个服务器上运行多个后台程序: 可以在服务器上运行 Redis 守护进程作会话存储。
一个后台程序运行在多台服务器上: 采用一个专用 Redis 服务器作会话存储。
多服务器,多程序集群: 使用粘性会话。
这些都是现有的应够很好应对常见场景的策略,你不必采用其它策略。
也许你会说:我要应对那些规模可能变得很大的“未来场景”。然而,在实际运用中,延后替换会话机制微不足道。你只需要在会话状态转变时,为每个用户注销一次就好,而没必要使用 JWT ,特别是考虑到接下来我要说的 JWT 的缺点。
易用
并不是这样。使用 JWT 的话,你得在服务器端和客户端自己实现会话管理。然而,标准的 Cookie 是拆箱即用的,JWT 可没这么简单使用。
更灵活
我现在还没看到有人真正地解释,使用 JWT 是如何更灵活的。基本上所有主流的会话管理机制,都让你随意存储会话数据。这和 JWT 的工作方式没什么两样。要我说,只是流行方式更灵活吧。如果你有什么不同的看法,欢迎联系我。
更安全
很多认为 JWT “更安全”,因为它加密了。虽然签名的 Cookie 比没签名的 Cookie 更安全,但这并非 JWT 所特有的。而好的会话实现,也会使用签名的 Cookie。
“使用加密”并不能让某件事更安全,当且仅当为特定服务加密时,这样说才有意义。事实是,不正当地使用加密,可能还更不安全。
我听到更多关于 JWT 更安全的论调是:“JWT 不作为 Cookie 发送给客户端”。这纯粹鬼扯,Cookie 只是 HTTP 的一个头信息,用 Cookie 并没有不安全。实际上,Cookie 能预防客户端恶意代码攻击,我稍后会讲到。
如果你担心有人会截获你的会话 Cookie,你大可以用 TLS。所有不使用 TLS 的会话机制都可能被人截获,包括 JWT。
内置过期功能
这毫无意义,而且也见得是什么有用的特性。服务端可以很好地实现过期功能,很多会话实现也是这样做的。而且,事实是,服务端实现会话过期,比依赖 JWT 过期功能实现的会话机制更好。因为,服务端实现会话过期,可以在你不需要会话信息时,把会话清除掉,JWT 的有状态令牌可做不到这一点。
不需要用户同意使用 Cookie
大错特错。没有所谓的 “Cookie 法”。所谓的覆盖所有限制持久化标识的 Cookie 法,对于我们而言,没有太实质性作用。你能够想到的所有的会话机制,已经涵盖了“Cokie 法”。
概括地说:
- 如果你出于功能性需求,使用会话或者令牌(比如,用户登录登出),那你就不需要向用户同意使用 Cookie ,不管你是如何保存会话的。
- 如果你出于其它目的,需要使用会话或者令牌(比如,分析或跟踪),那你也不需要用户同意使用 Cookie,不管你如何存储会话。
预防 CSRF
这个真没有。大抵有两种方式存储 JWT:
- 保存在 Cookie 中: 这样,你还是可能遭受 CSRF 攻击,还是得想办法预防。
- 保存在其它地方,如,本地存储中: 这样,你就不容易遭受 CSRF 攻击。不过这样的话,你的应用或者网站就得启用 JavaScript,你更容易受其它形式的攻击,详细信息待会讲到。
唯一能够减缓 CSRF 攻击方式是使用 CSRF 令牌,相关的会话机制这里不作描述。
更利于移动端使用
扯。所有的移动端浏览器都支持 Cookie以及会话,那些主流的移动端框架和 HTTP 库同样如此。所谓“更利于”不成立。
适用于禁用 Cookie 的用户
不大可能。用户不仅仅会拒绝 Cookie,他们还会拒绝其它所有形式的持久性玩意儿。包括本地存储和其它形式的持久化会话的东西(那些使用或者不使用 JWT 的方式),你用不用 JWT 都没差。这里需要讨论的是另外一个问题了——试图在禁用 Cookie 的情况下获取会话信息才是主因。
最重要的是,禁用 Cookie 的用户通常明白,这样做他们将无法进行身份认证,他们会为自己关心的网站单独打开 Cookie。这也不是你一个 Web 开发者需要解决的问题,更好的解决方式是,你要告知你的用户,为什么你的网站需要 Cookie。
缺点
至此,我已经就上述 JWT 所谓的优点一一反驳。你可能在想“哦,没什么大不了的,我用 JWT 又没有什么关系,哪怕我不那么需要 JWT。” 我想说,你错了。使用 JWT 做会话管理真的有很多缺点,有些甚至有严重的安全问题。
占据更多空间
JWT 令牌一点也不小。特别是,当你使用无状态的 JWT 时,所有的会话信息都被编码成一个令牌。很快,你就会发现,它们会超过浏览器对 Cookie 或者 URL 的数据大小限制。你可能会想到使用本地存储代替令牌,然而。。。。
更不安全
当你把 JWT 存储在 Cookie 中,它和其它的会话标识没差。但是,如果你把 JWT 保存在别的什么地方,你就更容易遭受其它形式的攻击。像这篇文章里提及的(特别是“会话存储”那一小节)。
我们从刚刚停下的地方继续吧:回到本地存储中来。本地存储(local storge)是 HTML5 的一个超级棒的功能,它允许我们存储键/值对到浏览器和 Cookie 中去。那么,我们应该把 JWT 保存在本地吗?考虑到 JWT 令牌可能到达的大小,这样做似乎有意义。通常,Cookie 的大小最多可以达到 4k 。当 JWT 令牌很大的时候,Cookie 的问题会暴露,本地存储正好能解决该问题。可是,本地存储不能提供 Cookie 机制那样的安全机制。
和 Cookie 不同,本地存储无法在每次请求时,把你存储的信息带上。获取存储信息的唯一方法是,使用 JavaScript,这也就意味着,任何通过安全策略的 JavaScript 攻击者,都能够访问和获取你本地存储的内容。不止如此,JavaScript 也不管你的数据是否通过 HTTPS 发送,就 JS 而言,它们和网页上的其它元素一样,都只是数据。
毕竟那些工程师们,绞尽脑汁,克服万难,以确保没人能够动我们的 Cookie。而我们,却置他们给予我们的礼物不顾。可能是我思想落后了吧,我一直想不明白为什么有人会这么做。
无法让 JWT 令牌单独失效
还有更多的安全问题要说。不像 Session,服务器可以在任何时候,使一个会话过期,却不能使一个无状态 JWT 过期。根据开发者的设计,不管发生了什么,我们只能等到 JWT 令牌自己过期。这样就意味着,你无法在黑客攻击了你的系统后,主动使会话过期;当用户修改了密码,你也无法使旧的会话过期。
实质上,你什么也做不了。如果不构建一个识别令牌的系统,你根本无法“杀死”一个会话。
数据过时
某种程度上,和这个问题相关的都存在安全问题。在缓存中的数据,最终都会过时,不再和你数据库中的数据一一对应。
这就好比,一个令牌中包含过时的信息,比如持有一个已经更改个人信息的用户 的旧的 URL。更严重的是,某人在你收回管理员权限时,可能依然持有管理员权限。在没有关闭系统的情况下,你无法拒绝这些人的管理员权限。
实现没有实战性,或者根本不存在
你可能会想,这些问题大都存在于无状态的 JWT 令牌中,你只对了大部分。可是,有状态的令牌和 Session 的 Cookie 一样,但没经历过实战性的考验。
已存的会话机制(举个例子,express-session)已经运用于生产环境,很多很多年了,它们的安全机制也因此提升。你使用 JWT 做会话管理没什么好处,要么你得自己实现会话的那一套东西,往往你实现的有很大的安全性问题;要么你使用第三方实现,但这种在现实中又不常见。
结论
无状态的 JWT 令牌无法灵活地更新或失效,也会因你的存储方式,或是有大小的限制,或是有安全性问题。有状态的 JWT 令牌和 Session-Cookie 作用相同,但是未经时间考验,并且没有完美的第三方客户端支持。
除非你做到 Reddit 这种规模,否则没有理由使用 JWT 令牌作为会话凭证,就用 Session 吧。
那么… JWT有什么好处吗
在文章的开始,我说过,JWT 在某些场合下有优势,但它不适合做会话机制。这个观点依然成立,当作为单用户认证时,JWT 特别有用。
我从JSON Web Token 说明中摘录了下面的话:
JSON Web Token(JWT) 是一种紧凑的、URL 安全的方式,在两方中传送声明。通过消息认证码(MAC)或加密技术,是声明能被数字签名或者完整性保护。
在上文中,“声明”可以用来指代某种“命令”,一次认证或者像下面这张场景:
你好,服务器 B,服务器 A 告诉我我可以 <这里是声明> ,给,这是(加密的)证明
举个例子,你可能开发了一个保存文件的服务,用户必须授权才能下载他们的文件,但是这些文件被分散到无状态的多个下载服务器。在这种情况下,你可能需要你的应用服务器(服务器 A)发布一个一次性的“下载令牌”。然后,用户就可以拿这个令牌去文件服务器(服务器 B)下载文件了。
在使用 JWT 时,有下面几种特定条件:
- 令牌的生命周期短 它们在几分钟后就过期,这段时间用作初始化下载
- 令牌只希望用户使用一次 应用服务器每次下载都会分配一个新的令牌,所有,一个令牌对应一个文件下载请求,然后,舍弃该令牌,不需要存储状态。
- 服务器端依然使用 Session 只是下载服务器需要令牌来认证每次下载请求,所以不需要存储状态。
当你看到这儿,我们绝对有理由混用 JWT 令牌和 Session。它们都有自己的适用场景,有时候,你需要使用它们俩。但是,不要用 JWT 做持久化、长期保存数据。
Readmore
深入理解JWT的使用场景和优劣
JSON Web Token (JWT)
Stop using JWT for sessions, part 2: Why your solution doesn’t work
Stop using JWT for sessions
用户认证:基于jwt和session的区别和优缺点
JWT 超详细分析
谈谈我对session, cookies和jwt的理解