使 JSON Web 令牌无效

IT技术 javascript node.js session session-cookies jwt
2021-02-06 23:47:24

对于我正在处理的新 node.js 项目,我正在考虑从基于 cookie 的会话方法切换(我的意思是,将 id 存储到包含用户浏览器中用户会话的键值存储)到使用 JSON Web 令牌 (jwt) 的基于令牌的会话方法(无键值存储)。

该项目是一个利用 socket.io 的游戏 - 在单个会话(web 和 socket.io)中将有多个通信通道的情况下,具有基于令牌的会话将非常有用

如何使用 jwt 方法从服务器提供令牌/会话失效?

我还想了解使用这种范式应该注意哪些常见(或不常见)的陷阱/攻击。例如,如果此范例容易受到与基于会话存储/cookie 的方法相同/不同类型的攻击。

所以,假设我有以下内容(改编自thisthis):

会话存储登录:

app.get('/login', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        // Create session token
        var token= createSessionToken();

        // Add to a key-value database
        KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});

        // The client should save this session token in a cookie
        response.json({sessionToken: token});
    });
}

基于令牌的登录:

var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
        response.json({token: token});
    });
}

——

会话存储方法的注销(或失效)将需要使用指定的令牌更新 KeyValueStore 数据库。

似乎这种机制在基于令牌的方法中不存在,因为令牌本身将包含通常存在于键值存储中的信息。

6个回答

我也一直在研究这个问题,虽然下面的想法都不是完整的解决方案,但它们可能会帮助其他人排除想法,或提供更多的想法。

1)只需从客户端删除令牌

显然这对服务器端安全没有任何影响,但它确实通过删除存在的令牌来阻止攻击者(即,他们必须在注销之前窃取令牌)。

2)创建令牌阻止列表

您可以将无效令牌存储到它们的初始到期日期,并将它们与传入请求进行比较。不过,这似乎否定了首先完全基于令牌的原因,因为您需要为每个请求访问数据库。不过,存储大小可能会更低,因为您只需要存储处于注销和到期时间之间的令牌(这是一种直觉,绝对取决于上下文)。

3)只要保持令牌到期时间短并经常轮换它们

如果您将令牌到期时间保持在足够短的时间间隔内,并让正在运行的客户端在必要时进行跟踪和请求更新,那么第 1 项将有效地作为一个完整的注销系统工作。这种方法的问题在于,它无法在客户端代码关闭之间保持用户登录(取决于您设置的到期间隔时间)。

临时计划

如果发生紧急情况,或者用户令牌被泄露,您可以做的一件事是允许用户使用他们的登录凭据更改底层用户查找 ID。这将使所有关联的令牌无效,因为将无法再找到关联的用户。

我还想指出,在令牌中包含上次登录日期是个好主意,这样您就可以在一段时间后强制重新登录。

就使用令牌攻击的异同而言,这篇文章解决了这个问题:https : //github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies -vs-token.markdown

优秀的方法。我的直觉是组合所有 3 个,和/或在每“n”个请求(而不是计时器)后请求一个新令牌。我们正在使用 redis 进行内存对象存储,我们可以轻松地将它用于案例 #2,然后延迟就会下降。
2021-03-26 23:47:24
可以通过将黑名单保存在内存中来提高黑名单的效率,这样只需命中 DB 即可记录失效并删除过期失效,并且仅在服务器启动时读取。在负载平衡架构下,内存黑名单可以在很短的时间间隔内轮询数据库,比如 10 秒,限制无效令牌的暴露。这些方法允许服务器在没有每个请求的数据库访问的情况下继续对请求进行身份验证。
2021-03-31 23:47:24
这篇文章写得很好,是2)上面的详细版本虽然它工作正常,但我个人认为与传统会话商店没有太大区别。我猜存储要求会更低,但您仍然需要一个数据库。JWT 对我最大的吸引力是根本不使用数据库进行会话。
2021-04-01 23:47:24
当用户更改其密码时使令牌无效的一种常见方法是使用其密码的散列对令牌进行签名。因此,如果密码更改,任何先前的令牌都会自动无法验证。您可以通过在用户记录中包含上次注销时间并使用上次注销时间和密码哈希的组合来签署令牌来将其扩展到注销。每次您需要验证令牌签名时,这都需要进行数据库查找,但据推测您无论如何都在查找用户。
2021-04-06 23:47:24
@TravisTerry 您的方法如果在单体应用程序中有用,但为了在微服务应用程序中实现它,您需要所有服务来存储用户的密码和上次登录或发出获取它们的请求,这两者都是坏主意。
2021-04-09 23:47:24

上面发布的想法很好,但是使所有现有 JWT 失效的一种非常简单易行的方法就是更改秘密。

如果您的服务器创建了 JWT,使用机密 (JWS) 对其进行签名,然后将其发送给客户端,只需更改机密将使所有现有令牌无效,并要求所有用户获得新令牌进行身份验证,因为他们的旧令牌突然变为无效根据到服务器。

它不需要对实际令牌内容(或查找 ID)进行任何修改。

显然,这仅适用于您希望所有现有令牌到期的紧急情况,对于每个令牌到期,需要上述解决方案之一(例如较短的令牌到期时间或使令牌中存储的密钥无效)。

我认为这种方法并不理想。虽然它可以工作并且当然很简单,但想象一下您使用公钥的情况 - 当您想要使单个令牌无效时,您不会想要去重新创建该密钥。
2021-03-11 23:47:24
@Signus - 有问题。不使用公钥作为秘密,但其他人可能依赖公钥来验证签名。
2021-03-12 23:47:24
@KijanaWoodard,公钥/私钥对可用于验证签名是否有效地是 RS256 算法中的秘密。在此处显示的示例中,他提到更改密钥以使 JWT 无效。这可以通过 a) 引入与签名不匹配的假公钥或 b) 生成新的公钥来完成。在那种情况下,它不太理想。
2021-03-23 23:47:24
这是非常糟糕的解决方案。使用 JWT 的主要原因是它是无状态和可扩展的。使用动态秘密会引入一个状态。如果服务跨多个节点集群,则每次发布新令牌时都必须同步密钥。您必须将机密存储在数据库或其他外部服务中,这只是重新发明基于 cookie 的身份验证
2021-03-23 23:47:24
@TuomasToivonen,但您必须使用密钥签署 JWT,并且能够使用相同的密钥验证 JWT。因此,您必须将机密存储在受保护的资源上。如果机密被泄露,您必须更改它并将该更改分发到您的每个节点。具有集群/扩展功能的托管服务提供商通常允许您在他们的服务中存储机密,以便轻松可靠地分发这些机密。
2021-03-25 23:47:24

这主要是支持@mattway答案并建立在其基础上的长评论

鉴于:

此页面上的一些其他建议解决方案主张在每个请求上都访问数据存储。如果您点击主数据存储来验证每个身份验证请求,那么我认为使用 JWT 而不是其他已建立的令牌身份验证机制的理由就更少了。如果您每次访问数据存储,您基本上已经使 JWT 成为有状态的,而不是无状态的。

(如果您的站点收到大量未经授权的请求,那么 JWT 会拒绝它们而不会访问数据存储,这很有帮助。可能还有其他类似的用例。)

鉴于:

对于典型的真实世界的 Web 应用程序,无法实现真正​​的无状态 JWT 身份验证,因为无状态 JWT 无法为以下重要用例提供即时安全的支持:

  • 用户的帐户被删除/阻止/暂停。

  • 用户密码已更改。

  • 用户的角色或权限已更改。

  • 用户已被管理员注销。

  • JWT 令牌中的任何其他应用程序关键数据都由站点管理员更改。

在这些情况下,您不能等待令牌到期。令牌失效必须立即发生。此外,您不能相信客户端不会保留和使用旧令牌的副本,无论是否具有恶意。

所以:

我认为来自@matt-way #2 TokenBlackList 的答案将是将所需状态添加到基于 JWT 的身份验证的最有效方法。

您有一个黑名单,用于保存这些令牌,直到它们到期为止。与用户总数相比,令牌列表将非常小,因为它只需要保留列入黑名单的令牌直到它们到期。我将通过将无效的令牌放入 redis、memcached 或其他支持在键上设置过期时间的内存数据存储中来实现。

对于通过初始 JWT 身份验证的每个身份验证请求,您仍然必须调用内存数据库,但您不必在其中存储整个用户集的密钥。(对于给定的站点来说,这可能是也可能不是什么大问题。)

@Julian 我不同意您的不同意见 :) JWT 解决了(对于服务)问题,需要访问为任何给定客户端提供授权信息的集中式实体。因此,不是服务 A 和服务 B 必须访问某些资源来确定客户端 X 是否有权限做某事,而是服务 A 和 B 从 X 接收一个令牌,证明他/她的权限(最常见的是由第三派对)。无论如何,JWT 是一种有助于避免系统中服务之间共享状态的工具,尤其是当它们由多个服务提供者控制时。
2021-03-17 23:47:24
如果每次都命中 DB,那么刷新令牌的目的就失去了。
2021-03-31 23:47:24
@Julian 你能详细说明一下吗?那么 JWT 真正解决了哪个问题呢?
2021-04-05 23:47:24
@zero01alpha 身份验证:这是使用 JWT 的最常见场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。信息交换:JSON Web Tokens 是一种在各方之间安全传输信息的好方法。因为 JWT 可以签名,所以您可以确定发件人就是他们所说的那样。jwt.io/介绍
2021-04-06 23:47:24
我不同意你的回答。命中数据库不会使任何有状态;在后端存储状态确实如此。JWT 的创建不是为了让您不必每次请求都访问数据库。使用 JWT 的每个主要应用程序都由数据库支持。JWT 解决了一个完全不同的问题。en.wikipedia.org/wiki/Stateless_protocol
2021-04-08 23:47:24

我会在用户模型上保留 jwt 版本号的记录。新的 jwt 令牌会将其版本设置为此。

当您验证 jwt 时,只需检查它的版本号是否等于用户当前的 jwt 版本。

任何时候您想使旧的 jwt 失效,只需增加用户的 jwt 版本号即可。

我同意@SergioCorrea 这将使 JWT 几乎与任何其他令牌身份验证机制一样有状态。
2021-03-11 23:47:24
这是一个有趣的想法,唯一的问题是存储版本的位置,作为令牌目的的一部分,它是无状态的并且不需要使用数据库。硬编码的版本会使其难以碰撞,而数据库中的版本号会抵消使用令牌的一些好处。
2021-03-13 23:47:24
我不应该这么说,因为在很多情况下,您可能会使用带有根本不涉及数据库的验证的令牌。但我认为在这种情况下很难避免。
2021-03-23 23:47:24
大概你已经在你的令牌中存储了一个用户 ID,然后查询数据库以检查用户是否存在/被授权访问 api 端点。因此,您不会通过将 jwt 令牌版本号与用户的版本号进行比较来执行任何额外的数据库查询。
2021-03-24 23:47:24
如果用户从多个设备登录怎么办?应该在所有令牌中使用一个令牌还是应该登录使所有以前的令牌无效?
2021-03-24 23:47:24

还没有尝试过,它使用了基于其他一些答案的大量信息。这里的复杂性是避免每次请求用户信息时调用服务器端数据存储。大多数其他解决方案需要对每个对用户会话存储的请求进行数据库查找。在某些情况下这很好,但这是为了避免此类调用并使所需的服务器端状态非常小而创建的。您最终将重新创建一个服务器端会话,无论它多小以提供所有强制失效功能。但如果你想这样做,这里的要点是:

目标:

  • 减少使用数据存储(无状态)。
  • 能够强制注销所有用户。
  • 能够随时强制注销任何个人。
  • 能够在一定时间后要求重新输入密码。
  • 能够与多个客户合作。
  • 能够在用户单击特定客户端的注销时强制重新登录。(为了防止有人在用户离开后“取消删除”客户端令牌 - 请参阅评论以获取更多信息)

解决方案:

  • 使用短期(<5m)访问令牌与较长寿命(几个小时)客户端存储的refresh-token配对
  • 每个请求都会检查身份验证或刷新令牌到期日期的有效性。
  • 当访问令牌过期时,客户端使用刷新令牌来刷新访问令牌。
  • 在刷新令牌检查期间,服务器检查用户 id 的小黑名单 - 如果发现则拒绝刷新请求。
  • 当客户端没有有效(未过期)刷新或身份验证令牌时,用户必须重新登录,因为所有其他请求都将被拒绝。
  • 在登录请求时,检查用户数据存储是否禁止。
  • 注销时 - 将该用户添加到会话黑名单中,以便他们必须重新登录。您必须存储其他信息,以免在多设备环境中将他们从所有设备中注销,但可以通过将设备字段添加到用户黑名单。
  • 要在 x 时间后强制重新输入 - 在身份验证令牌中维护上次登录日期,并根据请求进行检查。
  • 强制注销所有用户 - 重置令牌哈希键。

这需要您在服务器上维护一个黑名单(状态),假设用户表包含被禁止的用户信息。无效会话黑名单 - 是用户 ID 列表。此黑名单仅在刷新令牌请求期间检查。只要刷新令牌 TTL,条目就必须存在于其上。一旦刷新令牌过期,用户将需要重新登录。

缺点:

  • 仍然需要对刷新令牌请求进行数据存储查找。
  • 对于访问令牌的 TTL,无效令牌可能会继续运行。

优点:

  • 提供所需的功能。
  • 在正常操作下,刷新令牌操作对用户是隐藏的。
  • 只需要对刷新请求而不是每个请求进行数据存储查找。即每 15 分钟 1 个,而不是每秒 1 个。
  • 将服务器端状态最小化到一个非常小的黑名单。

使用此解决方案,不需要像 reddis 这样的内存数据存储,至少不需要用户信息,因为服务器每 15 分钟左右才进行一次数据库调用。如果使用 reddis,在其中存储有效/无效的会话列表将是一个非常快速和简单的解决方案。不需要刷新令牌。每个身份验证令牌都有一个会话 ID 和设备 ID,它们可以在创建时存储在 reddis 表中,并在适当时失效。然后他们将在每个请求上进行检查,并在无效时被拒绝。

或者您可以从会话/本地存储或 cookie 中删除您的 JWT。
2021-03-16 23:47:24
谢谢@Ashtonian。在进行了广泛的研究之后,我放弃了 JWT。除非您不遗余力地保护密钥,或者除非您委托给安全的 OAuth 实现,否则 JWT 比常规会话更容易受到攻击。查看我的完整报告:by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html
2021-03-22 23:47:24
一个人从一台电脑上起来让另一个人使用同一台电脑的场景呢?第 1 个人将退出并期望退出立即阻止第 2 个人。如果第二个人是普通用户,则客户端可以通过删除令牌轻松阻止该用户。但是,如果第二个用户具有黑客技能,则该用户有时间恢复仍然有效的令牌以作为第一个用户进行身份验证。似乎没有办法避免立即使令牌无效的需要。
2021-03-23 23:47:24
使用刷新令牌是允许列入黑名单的关键。很好的解释:auth0.com/blog/...
2021-03-25 23:47:24
在我看来,这似乎是最好的答案,因为它结合了短期访问令牌和可以列入黑名单的长期刷新令牌。注销时,客户端应删除访问令牌,以便第二个用户无法访问(即使在注销后访问令牌将在几分钟内保持有效)。@Joe Lapp 说黑客(第二个用户)即使在访问令牌被删除后也会得到它。如何?
2021-04-07 23:47:24