ChatGPT解决这个技术问题 Extra ChatGPT

使 JSON Web 令牌无效

对于我正在处理的新 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 数据库。

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

如果您使用的是“express-jwt”包,您可以查看 isRevoked 选项,或尝试复制相同的功能。 github.com/auth0/express-jwt#revoked-tokens
考虑在访问令牌上使用较短的过期时间并使用具有长期过期的刷新令牌,以允许检查用户在数据库中的访问状态(黑名单)。 auth0.com/blog/…
另一种选择是在有效负载中附加 IP 地址,同时生成 jwt 令牌并检查存储的 IP 与相同 IP 地址的传入请求。例如:nodeJs 中的 req.connection.remoteAddress。有些 ISP 提供商不会为每个客户发布静态 IP,我认为除非客户重新连接到互联网,否则这不会成为问题。
我倾向于说正是这个问题使 JWT 不是很有用。这是一个很酷的解决方案,也是一种安全地打破从不信任客户端规则的优雅方式,但最终解决了它的问题,主要是失效,总是不可避免地重新引入状态
有一种简单、快速且优雅的方法可以执行您期望 JWT 失效应该执行的操作 - 将用户注销。请参阅下面的answer

M
Matt Way

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

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

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

2)创建令牌阻止列表

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

3) 只需缩短令牌到期时间并经常轮换它们

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

临时计划

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

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

关于使用令牌进行攻击的相似/不同之处,这篇文章解决了以下问题:https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies-vs-token.markdown


这篇文章写得很好,是上面 2) 的详细版本。虽然它工作得很好,但我个人认为与传统的会话存储没有太大区别。我想存储要求会更低,但你仍然需要一个数据库。 JWT 对我最大的吸引力是完全不使用数据库进行会话。
当用户更改密码时,使令牌失效的一种常用方法是使用密码的哈希对令牌进行签名。因此,如果密码更改,任何以前的令牌都会自动验证失败。您可以通过在用户记录中包含上次注销时间并使用上次注销时间和密码哈希的组合来对令牌进行签名,从而将其扩展到注销。每次您需要验证令牌签名时,这都需要进行数据库查找,但大概您正在查找用户。
黑名单可以通过将其保存在内存中来提高效率,因此只需点击数据库即可记录失效并删除过期的失效,并且仅在服务器启动时读取。在负载均衡架构下,内存中的黑名单可以在很短的时间间隔(比如 10 秒)轮询 DB,从而限制无效令牌的暴露。这些方法允许服务器继续验证请求,而无需访问每个请求的数据库。
@TravisTerry您的方法如果在单体应用程序中有用,但为了在微服务应用程序中实现它,您需要所有服务来存储用户密码和上次登录或发出请求以获取它们,这两者都是坏主意。
另一种方法是为每个用户使用一个随机秘密,并将该秘密与用户一起保存在数据库中。当用户注销或更改密码时,只需更改数据库中的秘密。
A
Andy

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

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

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

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


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

这主要是支持和构建于 answer by @mattway 的长评论

鉴于:

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

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

鉴于:

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

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

用户密码已更改。

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

用户已被管理员注销。

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

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

所以:

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

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

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


我不同意你的回答。访问数据库不会使任何东西成为有状态的;在您的后端存储状态。没有创建 JWT,因此您不必在每次请求时都访问数据库。使用 JWT 的每个主要应用程序都由数据库支持。 JWT 解决了一个完全不同的问题。 en.wikipedia.org/wiki/Stateless_protocol
@Julian 你能详细说明一下吗?那么 JWT 真正解决了哪些问题呢?
@zero01alpha 身份验证:这是使用 JWT 的最常见场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。因为 JWT 可以签名,所以您可以确定发件人就是他们所说的那个人。请参阅jwt.io/introduction
@Julian 我不同意您的不同意见:) JWT 解决了(对于服务)需要访问为任何给定客户端提供授权信息的集中实体的问题。因此,不是服务 A 和服务 B 必须访问某些资源来确定客户端 X 是否有权做某事,而是服务 A 和 B 从 X 接收一个令牌,证明他/她的权限(最常见的是由 3rd派对)。无论如何,JWT 是一种有助于避免系统中服务之间共享状态的工具,尤其是当它们由多个服务提供商控制时。
同样来自 jwt.io/introduction If the JWT contains the necessary data, the need to query the database for certain operations may be reduced, though this may not always be the case.
D
DaftMonk

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

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

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


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

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

目标:

减少使用数据存储(无状态)。

能够强制注销所有用户。

能够随时强制注销任何个人。

能够在一定时间后要求重新输入密码。

能够与多个客户合作。

当用户从特定客户端单击注销时强制重新登录的能力。 (为了防止有人在用户离开后“取消删除”客户端令牌 - 请参阅评论以获取更多信息)

解决方案:

使用短期 (<5m) 访问令牌与长期(几个小时)客户端存储的刷新令牌配对。

每个请求都会检查身份验证或刷新令牌到期日期的有效性。

当访问令牌过期时,客户端使用刷新令牌刷新访问令牌。

在刷新令牌检查期间,服务器会检查一个小的用户 ID 黑名单 - 如果发现则拒绝刷新请求。

当客户端没有有效(未过期)的刷新或身份验证令牌时,用户必须重新登录,因为所有其他请求都将被拒绝。

在登录请求时,检查用户数据存储是否被禁止。

注销时 - 将该用户添加到会话黑名单,以便他们必须重新登录。您必须存储其他信息才能不将他们从多设备环境中的所有设备中注销,但可以通过将设备字段添加到用户黑名单。

要在 x 时间后强制重新进入 - 在身份验证令牌中维护上次登录日期,并根据请求进行检查。

要强制注销所有用户 - 重置令牌哈希键。

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

缺点:

仍然需要对刷新令牌请求进行数据存储查找。

对于访问令牌的 TTL,无效令牌可能会继续运行。

优点:

提供所需的功能。

在正常操作下,刷新令牌操作对用户隐藏。

只需要对刷新请求而不是每个请求进行数据存储查找。即每 15 分钟 1 次,而不是每秒 1 次。

将服务器端状态最小化为一个非常小的黑名单。

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


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

我一直在考虑的一种方法是在 JWT 中始终有一个 iat(发布于)值。然后,当用户注销时,将该时间戳存储在用户记录中。验证 JWT 时,只需将 iat 与上次注销的时间戳进行比较。如果 iat 较旧,则它无效。是的,你必须去数据库,但如果 JWT 在其他方面有效,我总是会提取用户记录。

我看到的主要缺点是,如果他们在多个浏览器中,或者也有移动客户端,它会将他们从所有会话中注销。

这也可能是使系统中的所有 JWT 失效的一个很好的机制。部分检查可能针对最后一次有效 iat 时间的全局时间戳。


好主意!解决“一个设备”的问题是让它成为一种应急功能,而不是注销。将日期存储在用户记录上,该日期会使之前发布的所有令牌无效。像 token_valid_after 之类的东西。惊人的!
像所有其他提出的解决方案一样,这个需要数据库查找,这就是这个问题存在的原因,因为避免查找是这里最重要的事情! (性能、可扩展性)。在正常情况下,您不需要数据库查找来获取用户数据,您已经从客户端获得了它。
这可以很容易地与刷新令牌方法 suggested in a different answer 结合使用,以减少对数据存储的访问次数。
M
Matas Kairaitis

我在这里有点晚了,但我认为我有一个不错的解决方案。

我的数据库中有一个“last_password_change”列,用于存储上次更改密码的日期和时间。我还将发布日期/时间存储在 JWT 中。验证令牌时,我会检查在颁发令牌后密码是否已更改,如果是,即使令牌尚未过期,令牌也会被拒绝。


你如何拒绝令牌?你能展示一个简短的示例代码吗?
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
需要数据库查找!
A
Aman Kumar Gupta

----------------这个答案有点晚了,但可能会对某人有所帮助----------------

在客户端,最简单的方法是从浏览器的存储中删除令牌。

但是,如果你想销毁节点服务器上的令牌怎么办 -

JWT 包的问题在于它没有提供任何方法或方式来销毁令牌。对于上面提到的 JWT,您可以使用不同的方法。但是我在这里使用 jwt-redis。

因此,为了销毁服务器端的令牌,您可以使用 jwt-redis 包而不是 JWT

这个库 (jwt-redis) 完全重复了库 jsonwebtoken 的全部功能,并增加了一个重要的功能。 jwt-redis 允许您将 tokenIdentifier 存储在 redis 中以验证有效性。 redis 中缺少 tokenIdentifier 会导致 token 无效。在jwt-redis中销毁token,有destroy方法

它以这种方式工作:

从 npm 安装 jwt-redis 创建:

var redis = require('redis'); var JWTR = require('jwt-redis').default; var redisClient = redis.createClient(); var jwtr = new JWTR(redisClient);常量秘密 = '秘密';常量 tokenIdentifier = '测试';常量负载 = { jti: tokenIdentifier }; // 您也可以将其他数据放入有效负载中 jwtr.sign(payload, secret) .then((token)=>{ // 你的代码 }) .catch((error)=>{ // 错误处理 });

验证:

jwtr.verify(令牌,秘密);

摧毁:

// 如果 jti 在令牌签名期间传递,则 tokenIdentifier else token jwtr.destroy(tokenIdentifier or token)

笔记 :

1)。您可以在令牌登录期间提供 expiresIn ,就像在 JWT 中提供的一样。

2)。如果在令牌签名期间未传递 jti,则 jti 由库随机生成。

可能这会对您或其他人有所帮助。谢谢。


这种方法似乎是最好的建议。但是,如果我有多个服务器,那么只要有变化,我就必须同步令牌标签,对吧?
谢谢@Eren,是的,如果您有多个服务器并且令牌的配置有任何更改,那么您当然需要再次同步它。还有一种方法是通过调用 jwtr.destroy(jti)(“jti 是 json 令牌标识符”)通过标签销毁令牌,您可以在签署令牌时将其传递到有效负载中,如果未通过,则库将随机生成 jti令牌。有关详细信息,请参阅此处“npmjs.com/package/jwt-redis
@Eren 也更新了建议的答案。谢谢!
N
NickVarcha

您可以在用户文档/记录的数据库中拥有“last_key_used”字段。

当用户使用 user 登录并通过时,生成一个新的随机字符串,将其存储在 last_key_used 字段中,并在签署令牌时将其添加到有效负载中。

当用户使用令牌登录时,检查数据库中的 last_key_used 以匹配令牌中的那个。

然后,例如,当用户注销时,或者如果您想使令牌无效,只需将“last_key_used”字段更改为另一个随机值,任何后续检查都将失败,从而迫使用户使用用户登录并再次通过。


这是我一直在考虑的解决方案,但它有以下缺点:(1)您要么对每个请求进行数据库查找以检查随机性(取消使用令牌而不是会话的原因),要么您正在仅在刷新令牌过期后间歇性检查(防止用户立即注销或会话立即终止); (2) 注销将用户从所有浏览器和所有设备中注销(这不是通常预期的行为)。
当用户注销时,您无需更改密钥,仅当他们更改密码或 - 如果您提供 - 当他们选择从所有设备注销时
E
Eduardo

保持这样的内存列表

user_id   revoke_tokens_issued_before
-------------------------------------
123       2018-07-02T15:55:33
567       2018-07-01T12:34:21

如果您的令牌在一周内到期,则清除或忽略比这更早的记录。也只保留每个用户的最新记录。列表的大小将取决于您保留代币的时间以及用户撤销其代币的频率。仅在表更改时使用 db。应用程序启动时将表加载到内存中。


大多数生产站点在多台服务器上运行,因此此解决方案将不起作用。添加 Redis 或类似的 interpocess 缓存会使系统显着复杂化,并且往往带来的问题多于解决方案。
@user2555515 所有服务器都可以与数据库同步。是否每次都访问数据库是您的选择。你可以说它带来了什么问题。
M
Mark Essel

每个用户唯一的字符串,以及一起散列的全局字符串

这是一个例子:

HEADER:ALGORITHM & TOKEN TYPE

{
  "alg": "HS256",
  "typ": "JWT"
}
PAYLOAD:DATA

{
  "sub": "1234567890",
  "some": "data",
  "iat": 1516239022
}
VERIFY SIGNATURE

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload), 
  HMACSHA256('perUserString'+'globalString')
)

where HMACSHA256 is your local crypto sha256
  nodejs 
    import sha256 from 'crypto-js/sha256';
    sha256(message);

例如用法见 https://jwt.io(不确定他们处理动态 256 位机密)


更多细节就足够了
@giantas,我认为马克的意思是签名部分。因此,仅使用单个密钥对 JWT 进行签名,而是将其组合为每个客户端唯一的密钥。因此,如果您想使用户的所有会话无效,只需更改该用户的密钥,如果您要使系统中的所有会话无效,只需更改该全局单一密钥。
好一个,但显然,仍然需要数据库查找
d
davidkomer

为什么不直接使用 jti 声明(nonce)并将其作为用户记录字段存储在列表中(依赖于 db,但至少可以使用逗号分隔的列表)?无需单独查找,正如其他人所指出的那样,您可能还是想获取用户记录,这样您就可以为不同的客户端实例拥有多个有效令牌(“到处注销”可以将列表重置为空)


是的,这个。也许在用户表和新(会话)表之间建立一对多的关系,这样您就可以将元数据与 jti 声明一起存储。
S
Shamseer

聚会迟到了,经过一番研究,下面给出了我的两分钱。在注销期间,请确保正在发生以下事情...

清除客户端存储/会话

每当登录或注销分别发生时,更新用户表上次登录日期时间和注销日期时间。因此登录日期时间应始终大于注销(如果当前状态为登录且尚未注销,则保持注销日期为空)

这比保留额外的黑名单表和定期清除要简单得多。多设备支持需要额外的表格来保持登录、注销日期以及一些额外的细节,如操作系统或客户端细节。


J
James111

我是通过以下方式做到的:

生成唯一的哈希,然后将其存储在 redis 和您的 JWT 中。这可以称为会话我们还将存储特定 JWT 发出的请求数 - 每次将 jwt 发送到服务器时,我们都会增加请求整数。 (这是可选的)

因此,当用户登录时,会创建一个唯一的哈希值,存储在 redis 中并注入到您的 JWT 中。

当用户尝试访问受保护的端点时,您将从 JWT 中获取唯一的会话哈希,查询 redis 并查看它是否匹配!

我们可以从这里扩展并使我们的 JWT 更加安全,方法如下:

每个特定 JWT 发出的 X 请求,我们都会生成一个新的唯一会话,将其存储在我们的 JWT 中,然后将前一个会话列入黑名单。

这意味着 JWT 不断变化并阻止陈旧的 JWT 被黑客攻击、被盗或其他原因。


您可以对令牌本身进行哈希处理并将该值存储在 redis 中,而不是将新的哈希值注入令牌中。
另请查看 JWT 中的 audjti 声明,您走在正确的道路上。
E
Ebru Yener

为代币提供 1 天的到期时间 维护每日黑名单。将失效/注销令牌放入黑名单

对于令牌验证,首先检查令牌过期时间,如果令牌未过期,则检查黑名单。

对于长会话需求,应该有一种延长令牌到期时间的机制。


将代币放入黑名单,你的无国籍状态就消失了
如果你在数据库中维护 backlist,你仍然保持无状态
将黑名单放入数据库中,您的性能和可扩展性就在那里。
V
Valentin

Kafka消息队列和本地黑名单

我考虑过使用像 kafka 这样的消息传递系统。让我解释:

例如,您可以拥有一个负责 loginlogout 并生成 JWT 令牌的微服务(我们称之为 userMgmtMs 服务)。然后这个令牌被传递给客户端。

现在客户端可以使用此令牌调用不同的微服务(我们称之为 pricesMs),在 pricesMs 中,不会对触发初始令牌创建的 users 表进行数据库检查。此数据库必须仅存在于 userMgmtMs 中。此外,JWT 令牌应该包括权限/角色,以便价格无需从数据库中查找任何内容以允许 spring 安全性工作。

JwtRequestFilter 可以提供由 JWT 令牌中提供的数据创建的 UserDetails 对象(显然没有密码),而不是进入价格中的数据库。

那么,如何注销或使令牌无效?由于我们不想在每次请求priecesMs 时调用userMgmtMs 的数据库(这会引入很多不需要的依赖项),因此解决方案可能是使用这个令牌黑名单。

我建议使用 kafka 消息队列,而不是保持这个黑名单为中心并依赖所有微服务中的一个表。

userMgmtMs 仍然对 logout 负责,一旦完成,它将把它放入自己的黑名单(一个不在微服务之间共享的表)。此外,它将带有此令牌内容的 kafka 事件发送到订阅所有其他微服务的内部 kafka 服务。

一旦其他微服务收到 kafka 事件,它们也会将其放入内部黑名单中。

即使某些微服务在注销时关闭,它们最终也会再次启动并在稍后的状态下接收消息。

由于开发了 kafka,因此客户端可以自己参考他们确实阅读了哪些消息,因此可以确保没有客户端、down 或 up 会错过任何这些无效令牌。

我能想到的唯一问题是 kafka 消息传递服务将再次引入单点故障。但这有点颠倒了,因为如果我们有一个全局表,其中保存了所有无效的 JWT 令牌,并且这个数据库或微服务关闭,则没有任何作用。使用 kafka 方法 + 客户端删除 JWT 令牌以进行正常用户注销,在大多数情况下,kafka 的停机时间甚至不会被注意到。由于黑名单作为内部副本分布在所有微服务中。

在关闭的情况下,您需要使被黑客入侵的用户无效并且 kafka 已关闭,这就是问题开始的地方。在这种情况下,将秘密更改为最后的手段可能会有所帮助。或者在这样做之前确保 kafka 已启动。

免责声明:我还没有实现这个解决方案,但不知何故,我觉得大多数提议的解决方案都否定了 JWT 令牌具有中央数据库查找的想法。所以我在考虑另一种解决方案。

请让我知道您的想法,这是否有意义或者是否有明显的原因无法解释?


我同意你的做法。当我们谈论可能性时,MQ 比我们想象的要稳定得多。
我完成了这个想法的快乐之路,它似乎工作正常。微服务可以完全独立,只检查其本地的黑名单副本。如果他们下跌,他们将在再次上涨时收到所有消息,因此没有丢失消息的风险。假设 MQ 中没有故障。现在我正在研究如何进行 CSRF 保护的问题,不确定这将如何发挥作用。
我刚刚使用此信息将grantedAuthorities 和用户ID 添加到JWT,我可以使用WebSecurityConfigurerAdapter,就像在我的微服务(antMatchers 等)中拥有真实的用户数据库一样。如果用户角色和权限发生更改,则此需求还会触发一条无效消息到 MQ,以通知其他微服务当前具有包含权限的 JWT 令牌也无效。
A
Arik

如果您希望能够撤销用户令牌,您可以跟踪数据库上所有已发布的令牌,并检查它们在类似会话的表上是否有效(存在)。缺点是您将在每次请求时访问数据库。

我没有尝试过,但我建议使用以下方法来允许令牌撤销,同时将数据库命中率降至最低 -

要降低数据库检查率,请根据某种确定性关联将所有已发布的 JWT 令牌分成 X 组(例如,按用户 ID 的第一位数字划分为 10 个组)。

每个 JWT 令牌都将保存组 ID 和创建令牌时创建的时间戳。例如,{ "group_id": 1, "timestamp": 1551861473716 }

服务器将所有组 ID 保存在内存中,每个组都有一个时间戳,指示属于该组的用户的最后一次注销事件是什么时候。例如,{ "group1": 1551861473714, "group2": 1551861487293, ... }

将检查具有较旧组时间戳的 JWT 令牌的请求的有效性(DB 命中),如果有效,将发布具有新时间戳的新 JWT 令牌以供客户端将来使用。如果令牌的组时间戳更新,我们信任 JWT(无 DB 命中)。

所以 -

如果令牌具有旧的组时间戳,我们只使用数据库验证 JWT 令牌,而未来的请求将不会得到验证,直到用户组中的某人注销。我们使用组来限制时间戳更改的数量(比如有一个用户登录和退出就像没有明天一样 - 只会影响有限数量的用户而不是所有人)我们限制组的数量以限制内存中保存的时间戳数量使令牌无效是一件轻而易举的事——只需将其从会话表中删除并为用户组生成一个新的时间戳。


相同的列表可以保存在内存中(c# 的应用程序),这样就无需为每个请求访问数据库。该列表可以在应用程序启动时从 db 加载
u
user2555515

如果“从所有设备注销”选项是可以接受的(在大多数情况下是这样):

将令牌版本字段添加到用户记录。

将此字段中的值添加到存储在 JWT 中的声明中。

每次用户注销时增加版本。

在验证令牌时,将其版本声明与存储在用户记录中的版本进行比较,如果不同则拒绝。

在大多数情况下,无论如何都需要进行一次数据库之旅来获取用户记录,因此这不会给验证过程增加太多开销。与维护黑名单不同,在黑名单中,由于需要使用连接或单独调用、清理旧记录等,因此数据库负载很大。


O
Olumide

使用 JWT 的刷新...

我认为可行的一种方法是在数据库上存储刷新令牌(可以是 GUID)和对应的刷新令牌 ID(无论进行多少次刷新都不会改变),并将它们添加为正在生成用户的 JWT 时的用户。可以使用数据库的替代方案,例如内存缓存。但我在这个答案中使用了数据库。

然后,创建客户端可以在 JWT 到期之前调用的 JWT 刷新 Web API 端点。调用刷新时,从 JWT 中的声明中获取刷新令牌。

在对 JWT 刷新端点的任何调用中,验证当前刷新令牌和刷新令牌 ID 作为数据库上的一对。生成一个新的刷新令牌,并使用刷新令牌 ID 用它来替换数据库上的旧刷新令牌。请记住,它们是可以从 JWT 中提取的声明

从当前 JWT 中提取用户的声明。开始生成新 JWT 的过程。用新生成的刷新令牌替换旧刷新令牌声明的值,该刷新令牌也新保存在数据库中。有了所有这些,生成新的 JWT 并将其发送给客户端。

因此,在使用刷新令牌后,无论是预期用户还是攻击者,任何其他尝试在数据库上使用未配对的刷新令牌及其刷新令牌 ID,都不会导致生成新的 JWT,从而阻止具有该刷新令牌 ID 的任何客户端再使用后端,从而导致此类客户端(包括合法客户端)完全注销。

这解释了基本信息。

接下来要添加的是有一个窗口,用于刷新 JWT 的时间,以便该窗口之外的任何内容都是可疑活动。例如,窗口可以是 JWT 到期前 10 分钟。生成 JWT 的日期时间可以保存为该 JWT 本身的声明。并且当这种可疑活动发生时,即当其他人试图在窗口外或窗口内重用该刷新令牌 ID 后,它已经在窗口内使用过,应将刷新令牌 ID 标记为无效。因此,即使是刷新令牌 ID 的有效所有者也必须重新登录。

无法在数据库中找到与提供的刷新令牌 ID 配对的刷新令牌意味着刷新令牌 ID 应该无效。因为空闲用户可能会尝试使用攻击者已经使用过的刷新令牌。

如前所述,当用户尝试使用刷新令牌时,在目标用户之前被攻击者窃取和使用的 JWT 也会被标记为无效。

唯一未涵盖的情况是,即使攻击者可能已经窃取了客户端,客户端也从未尝试刷新其 JWT。但这不太可能发生在不受攻击者监管(或类似情况)的客户端上,这意味着攻击者无法预测客户端何时停止使用后端。

如果客户端启动通常的注销。应进行注销以从数据库中删除刷新令牌 ID 和相关记录,从而防止任何客户端生成刷新 JWT。


c
codeman48

以下方法可以提供两全其美的解决方案:

让“立即”表示“~1 分钟”。

案例:

用户尝试成功登录: A. 在令牌中添加“发布时间”字段,并根据需要保留到期时间。 B. 存储用户密码哈希的哈希或在用户表中创建一个新字段,例如 tokenhash。将令牌哈希存储在生成的令牌中。用户访问一个url: A.如果“发出时间”在“立即”范围内,则正常处理令牌。不要更改“发布时间”。根据“立即”的持续时间,这是一个容易受到攻击的持续时间。但是像一两分钟这样的短持续时间不应该太冒险。 (这是性能和安全性之间的平衡)。三是不需要在这里打分贝。 B. 如果令牌不在“立即”范围内,请根据数据库检查令牌哈希。如果没问题,请更新“发布时间”字段。如果不行,则不处理请求(最终强制执行安全性)。用户更改令牌哈希以保护帐户。在“立即”的将来,该帐户是安全的。

我们将数据库查找保存在“立即”范围内。如果在“立即”持续时间内有来自客户端的请求突发,这是最有益的。


这对于如何在没有数据库查找的情况下使用 jwts 很有用,但是如何强制注销呢?从数据库中删除 tokenhash
@funseiki 是的。在用户注销时,可以从客户端移除令牌,并将 tokenhash 更改为特殊值。在“立即”的未来,所有其他可能的副本也会变得无效,从而强制重新登录。缺点:它会从所有设备上注销用户(还没有想到如何克服这个问题)。
O
Olumide

不使用 JWT 的刷新...

我想到了 2 种攻击场景。一是关于登录凭据的泄露。另一个是 JWT 的实际盗窃。

对于受损的登录凭据,当发生新登录时,通常会向用户发送电子邮件通知。因此,如果客户不同意成为登录的人,则应建议他们重置凭据,这应将上次设置密码的日期时间保存到数据库/缓存中(并在用户在初始注册时设置密码)。每当授权用户操作时,应从数据库/缓存中获取用户更改密码的日期时间,并与生成给定 JWT 的日期时间进行比较,并禁止在所述日期之前生成的 JWT 的操作- 重置凭证的时间,因此基本上使此类 JWT 无用。这意味着将生成 JWT 的日期时间保存为 JWT 本身的声明。在 ASP.NET Core 中,可以使用策略/要求来进行此比较,如果失败,则禁止客户端。因此,每当完成凭据重置时,这都会在全局范围内注销后端用户。

对于 JWT 的实际盗窃...... JWT 的盗窃并不容易被发现,但过期的 JWT 很容易解决这个问题。但是如何在 JWT 过期之前阻止攻击者呢?它具有实际的全局注销。它类似于上面描述的凭据重置。为此,通常将用户启动全局注销的日期时间保存在数据库/缓存中,并在授权用户操作时,获取它并将其与给定 JWT 的生成日期时间进行比较,并禁止该操作在上述全局注销日期时间之前生成的 JWT,因此基本上使此类 JWT 无用。如前所述,这可以使用 ASP.NET Core 中的策略/要求来完成。

现在,您如何检测 JWT 被盗?我现在对此的回答是偶尔提醒用户全局注销并再次登录,因为这肯定会使攻击者注销。


E
Erisan Olasheni

使令牌无效的好方法仍然需要数据库访问。用于包括用户记录的某些部分何时更改的目的,例如更改角色、更改密码、电子邮件等。可以在用户记录中添加一个 modifiedupdated_at 字段,记录此更改的时间,然后将其包含在声明中。因此,当 JWT 被验证时,您将声明中的时间与数据库中记录的时间进行比较,如果声明的时间在之前,则令牌无效。这种方法也类似于将 iat 存储在数据库中。

注意:如果您使用的是 modifiedupdated_at 选项,那么您还必须在用户登录和注销时对其进行更新。


N
Normal

只需创建将以下对象添加到您的用户架构:

const userSchema = new mongoose.Schema({
{
... your schema code,
destroyAnyJWTbefore: Date
}

并且每当您在 /login 上收到 POST 请求时,将此文档的日期更改为 Date.now()

最后,在您的身份验证检查代码中,即在您检查 isAuthanticatedprotected 或您使用的任何名称的中间件中,只需添加一个验证以检查 myjwt.iat 是否大于 userDoc.destroyAnyJWTbefore

如果您想在服务器端销毁 JWT,则此解决方案在安全性方面是最好的。

该解决方案不再依赖于客户端,它打破了使用 JWT 的主要目标,即停止在服务器端存储令牌。

这取决于您的项目上下文,但很可能您希望从服务器销毁 JWT。

如果您只想从客户端销毁令牌,只需从浏览器中删除 cookie(如果您的客户端是浏览器),同样可以在智能手机或任何其他客户端上完成。

如果选择从服务器端销毁令牌,我建议您使用 Radis 快速执行此操作,通过实现其他用户提到的黑名单样式。

现在的主要问题是:JWT 没用吗?天知道。


顺便说一句,destroyAnyJWTbefore 的更好名称是“lastLogout”
要回答我的问题,JWT 没用吗?我会说不,因为在用户更新密码之前被盗令牌的安全风险非常低。正如那句老话“保持简单和愚蠢”,不要想太多。所以实际上不需要在服务器端销毁令牌。时期 (。)
T
Tharsanan

我将回答如果我们在使用 JWT 时需要从所有设备功能中提供注销功能。这种方法将对每个请求使用数据库查找。因为即使发生服务器崩溃,我们也需要持久的安全状态。在用户表中,我们将有两列

LastValidTime(默认:创建时间) 登录(默认:true)

每当用户发出注销请求时,我们会将 LastValidTime 更新为当前时间,并将 Logged-In 更新为 false。如果有登录请求,我们不会更改 LastValidTime,但 Logged-In 将设置为 true。

当我们创建 JWT 时,我们将在有效负载中拥有 JWT 创建时间。当我们授权服务时,我们将检查 3 个条件

JWT 是否有效 JWT 有效负载创建时间是否大于用户 LastValidTime 用户是否已登录

让我们看一个实际的场景。

用户 X 有两台设备 A、B。他在晚上 7 点使用设备 A 和设备 B 登录到我们的服务器。(假设 JWT 过期时间为 12 小时)。 A 和 B 都有 JWT 和 createdTime : 7pm

晚上 9 点,他丢失了设备 B。他立即从设备 A 注销。这意味着现在我们的数据库 X 用户条目的 LastValidTime 为“ThatDate:9:00:xx:xxx”,Logged-In 为“false”。

在 9:30,Mr.Thief 尝试使用设备 B 登录。即使登录为假,我们也会检查数据库,因此我们不会允许。

晚上 10 点,X 先生从他的设备 A 登录。现在设备 A 具有创建时间:晚上 10 点的 JWT。现在数据库登录设置为“真”

在晚上 10:30,Mr.Thief 尝试登录。即使 Logged-In 是真的。数据库中的 LastValidTime 是晚上 9 点,但 B 的 JWT 已将时间创建为晚上 7 点。所以他不会被允许访问该服务。因此,在没有密码的情况下使用设备 B,他无法在一台设备注销后使用已创建的 JWT。


C
Community

IAM 解决方案,如 Keycloak(我曾研究过)提供令牌撤销端点,如

令牌撤销端点 /realms/{realm-name}/protocol/openid-connect/revoke

如果您只是想注销用户代理(或用户),您也可以调用端点(这只会使令牌无效)。同样,在 Keycloak 的情况下,依赖方只需要调用端点

/realms/{realm-name}/protocol/openid-connect/logout

Link in case if you want to learn more


G
George I.

另一种方法是为关键 API 端点提供一个中间件脚本。如果令牌被管理员无效,此中间件脚本将检查数据库。对于不需要立即完全阻止用户访问的情况,此解决方案可能很有用。


M
MyDaftQuestions

在此示例中,我假设最终用户也有一个帐户。如果不是这种情况,那么该方法的其余部分不太可能奏效。

当您创建 JWT 时,将其保存在与正在登录的帐户关联的数据库中。这确实意味着您可以仅从 JWT 中提取有关用户的其他信息,因此取决于环境,这可能会也可能不会没事。

在之后的每个请求中,您不仅执行(我希望)您使用的任何框架附带的标准验证(验证 JWT 是否有效),它还包括用户 ID 或其他令牌(需要匹配数据库中的那个)。

当您注销时,删除 cookie(如果使用)并使数据库中的 JWT(字符串)无效。如果无法从客户端删除 cookie,那么至少注销过程将确保令牌被销毁。

我发现这种方法,再加上另一个唯一标识符(因此数据库中有 2 个持久项并且可供前端使用),会话非常有弹性


H
Hai Phaikawl

以下是无需在每个请求上调用数据库的方法:

将有效令牌的哈希图保存在内存缓存中(例如,大小有限的 LRU)

检查令牌时:如果令牌在缓存中,立即返回结果,不需要数据库查询(大多数情况)。否则执行全面检查(查询数据库,检查用户状态和无效令牌...)。然后更新缓存。

使令牌无效时:将其添加到数据库中的黑名单中,然后更新缓存,如果需要,向所有服务器发送信号。

请记住,缓存应该具有有限的大小,例如 LRU,否则您可能会耗尽内存。


Y
Yilmaz

即使您将令牌从存储中删除,它仍然有效,但仅在短时间内有效,以减少被恶意使用的可能性。

您可以创建一个 deny-listing,从存储中删除令牌后,您可以将令牌添加到此列表中。如果您有一个微服务服务,则使用此令牌的所有其他服务都必须添加额外的逻辑来检查此列表。这将集中您的身份验证,因为每个服务器都必须检查一个集中的数据结构。


J
Jose V

如果不对每个令牌验证进行数据库查找,这似乎真的很难解决。我能想到的替代方法是在服务器端保留一个无效令牌的黑名单;每当发生更改时,都应该在数据库上进行更新,以便在重新启动时保持更改,方法是让服务器在重新启动时检查数据库以加载当前的黑名单。

但是,如果您将它保存在服务器内存中(某种全局变量),那么如果您使用多个服务器,它将无法跨多个服务器进行扩展,因此在这种情况下,您可以将其保存在共享的 Redis 缓存中,这应该是设置以将数据保存在某处(数据库?文件系统?)以防它必须重新启动,并且每次启动新服务器时,它都必须订阅 Redis 缓存。

作为黑名单的替代方案,使用相同的解决方案,您可以使用每个会话保存在 redis 中的哈希来执行此操作,正如另一个 answer 指出的那样(但不确定对于许多用户登录会更有效)。

听起来是不是很复杂?它对我有用!

免责声明:我没有使用过 Redis。