对于我正在处理的新 node.js 项目,我正在考虑从基于 cookie 的会话方法切换(我的意思是,将 id 存储到包含用户浏览器中的用户会话的键值存储)到使用 JSON Web 令牌 (jwt) 的基于令牌的会话方法(无键值存储)。
该项目是一个使用 socket.io 的游戏 - 在单个会话中存在多个通信通道(web 和 socket.io)的情况下,具有基于令牌的会话将很有用
如何使用 jwt 方法从服务器提供令牌/会话失效?
我还想了解这种范式应该注意哪些常见(或不常见)的陷阱/攻击。例如,如果这种范式容易受到与基于会话存储/cookie 的方法相同/不同类型的攻击。
会话存储登录:
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 数据库。
在基于令牌的方法中似乎不存在这种机制,因为令牌本身将包含通常存在于键值存储中的信息。
isRevoked
选项,或尝试复制相同的功能。 github.com/auth0/express-jwt#revoked-tokens
我也一直在研究这个问题,虽然下面的想法都不是完整的解决方案,但它们可能会帮助其他人排除想法,或者提供更多的想法。
1)只需从客户端删除令牌
显然这对服务器端的安全没有任何帮助,但它确实通过从存在中删除令牌来阻止攻击者(即,他们必须在注销之前窃取令牌)。
2)创建令牌阻止列表
您可以将无效令牌存储到其初始到期日期,并将它们与传入请求进行比较。这似乎否定了首先完全基于令牌的原因,因为您需要为每个请求触摸数据库。不过,存储大小可能会更低,因为您只需要存储在注销和到期时间之间的令牌(这是一种直觉,并且绝对取决于上下文)。
3) 只需缩短令牌到期时间并经常轮换它们
如果您将令牌过期时间保持在足够短的时间间隔内,并让正在运行的客户端跟踪并在必要时请求更新,那么 1 号将有效地作为一个完整的注销系统工作。这种方法的问题在于,它无法让用户在关闭客户端代码之间保持登录状态(取决于您设置的到期间隔时间)。
临时计划
如果发生紧急情况,或者用户令牌被泄露,您可以做的一件事是允许用户使用他们的登录凭据更改底层用户查找 ID。这将使所有关联的令牌无效,因为不再能够找到关联的用户。
我还想指出,在令牌中包含上次登录日期是个好主意,这样您就可以在一段时间后强制重新登录。
关于使用令牌进行攻击的相似/不同之处,这篇文章解决了以下问题:https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies-vs-token.markdown
上面发布的想法很好,但是使所有现有 JWT 失效的一种非常简单的方法就是更改密钥。
如果您的服务器创建 JWT,使用机密 (JWS) 对其进行签名,然后将其发送给客户端,只需更改机密将使所有现有令牌无效,并要求所有用户获得新令牌以进行身份验证,因为他们的旧令牌突然变得无效根据到服务器。
它不需要对实际令牌内容(或查找 ID)进行任何修改。
显然,这仅适用于您希望所有现有令牌到期的紧急情况,因为每个令牌到期都需要上述解决方案之一(例如短令牌到期时间或使令牌内的存储密钥无效)。
这主要是支持和构建于 answer by @mattway 的长评论
鉴于:
此页面上的其他一些建议的解决方案主张在每个请求上访问数据存储。如果您点击主数据存储来验证每个身份验证请求,那么我认为使用 JWT 而不是其他已建立的令牌身份验证机制的理由更少。如果您每次都访问数据存储区,您实际上已经使 JWT 成为有状态的,而不是无状态的。
(如果您的站点收到大量未经授权的请求,那么 JWT 会在不访问数据存储的情况下拒绝它们,这很有帮助。可能还有其他类似的用例。)
鉴于:
对于典型的现实世界 Web 应用程序,无法实现真正的无状态 JWT 身份验证,因为无状态 JWT 无法为以下重要用例提供即时和安全的支持:
用户的帐户被删除/阻止/暂停。
用户密码已更改。
用户的角色或权限已更改。
用户已被管理员注销。
JWT 令牌中的任何其他应用程序关键数据都由站点管理员更改。
在这些情况下,您不能等待令牌到期。令牌失效必须立即发生。此外,您不能相信客户端不会保留和使用旧令牌的副本,无论是否有恶意。
所以:
我认为@matt-way 的答案#2 TokenBlackList 是向基于 JWT 的身份验证添加所需状态的最有效方法。
您有一个黑名单,其中包含这些令牌,直到它们的到期日期到期。与用户总数相比,令牌列表将非常小,因为它只需要保留列入黑名单的令牌直到它们到期。我会通过将无效的令牌放入 redis、memcached 或另一个支持在键上设置过期时间的内存数据存储中来实现。
对于通过初始 JWT 身份验证的每个身份验证请求,您仍然需要调用内存数据库,但您不必在其中存储整个用户集的密钥。 (对于给定的网站来说,这可能是也可能不是什么大问题。)
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.
我会在用户模型上记录 jwt 版本号。新的 jwt 令牌会将其版本设置为此。
当您验证 jwt 时,只需检查它的版本号是否等于用户当前的 jwt 版本。
任何时候你想使旧的 jwt 失效,只需增加用户的 jwt 版本号。
还没有尝试过,它使用了基于其他一些答案的大量信息。这里的复杂性是避免每次请求用户信息时都调用服务器端数据存储。大多数其他解决方案都需要对用户会话存储的每个请求进行数据库查找。在某些情况下这很好,但这是为了避免此类调用并使任何所需的服务器端状态非常小而创建的。您最终将重新创建一个服务器端会话,无论多么小以提供所有强制失效功能。但如果你想这样做,这里的要点是:
目标:
减少使用数据存储(无状态)。
能够强制注销所有用户。
能够随时强制注销任何个人。
能够在一定时间后要求重新输入密码。
能够与多个客户合作。
当用户从特定客户端单击注销时强制重新登录的能力。 (为了防止有人在用户离开后“取消删除”客户端令牌 - 请参阅评论以获取更多信息)
解决方案:
使用短期 (<5m) 访问令牌与长期(几个小时)客户端存储的刷新令牌配对。
每个请求都会检查身份验证或刷新令牌到期日期的有效性。
当访问令牌过期时,客户端使用刷新令牌刷新访问令牌。
在刷新令牌检查期间,服务器会检查一个小的用户 ID 黑名单 - 如果发现则拒绝刷新请求。
当客户端没有有效(未过期)的刷新或身份验证令牌时,用户必须重新登录,因为所有其他请求都将被拒绝。
在登录请求时,检查用户数据存储是否被禁止。
注销时 - 将该用户添加到会话黑名单,以便他们必须重新登录。您必须存储其他信息才能不将他们从多设备环境中的所有设备中注销,但可以通过将设备字段添加到用户黑名单。
要在 x 时间后强制重新进入 - 在身份验证令牌中维护上次登录日期,并根据请求进行检查。
要强制注销所有用户 - 重置令牌哈希键。
这需要你在服务器上维护一个黑名单(状态),假设用户表包含被禁止的用户信息。无效会话黑名单 - 是用户 ID 列表。仅在刷新令牌请求期间检查此黑名单。只要刷新令牌 TTL,条目就必须存在。一旦刷新令牌过期,用户将需要重新登录。
缺点:
仍然需要对刷新令牌请求进行数据存储查找。
对于访问令牌的 TTL,无效令牌可能会继续运行。
优点:
提供所需的功能。
在正常操作下,刷新令牌操作对用户隐藏。
只需要对刷新请求而不是每个请求进行数据存储查找。即每 15 分钟 1 次,而不是每秒 1 次。
将服务器端状态最小化为一个非常小的黑名单。
使用此解决方案,不需要像 reddis 这样的内存数据存储,至少不需要像您一样用于用户信息,因为服务器仅每 15 分钟左右进行一次 db 调用。如果使用 reddis,在其中存储一个有效/无效的会话列表将是一个非常快速和简单的解决方案。不需要刷新令牌。每个身份验证令牌都有一个会话 ID 和设备 ID,它们可以在创建时存储在 reddis 表中,并在适当时失效。然后将对每个请求进行检查,并在无效时拒绝。
我一直在考虑的一种方法是在 JWT 中始终有一个 iat
(发布于)值。然后,当用户注销时,将该时间戳存储在用户记录中。验证 JWT 时,只需将 iat
与上次注销的时间戳进行比较。如果 iat
较旧,则它无效。是的,你必须去数据库,但如果 JWT 在其他方面有效,我总是会提取用户记录。
我看到的主要缺点是,如果他们在多个浏览器中,或者也有移动客户端,它会将他们从所有会话中注销。
这也可能是使系统中的所有 JWT 失效的一个很好的机制。部分检查可能针对最后一次有效 iat
时间的全局时间戳。
token_valid_after
之类的东西。惊人的!
我在这里有点晚了,但我认为我有一个不错的解决方案。
我的数据库中有一个“last_password_change”列,用于存储上次更改密码的日期和时间。我还将发布日期/时间存储在 JWT 中。验证令牌时,我会检查在颁发令牌后密码是否已更改,如果是,即使令牌尚未过期,令牌也会被拒绝。
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
----------------这个答案有点晚了,但可能会对某人有所帮助----------------
在客户端,最简单的方法是从浏览器的存储中删除令牌。
但是,如果你想销毁节点服务器上的令牌怎么办 -
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 由库随机生成。
可能这会对您或其他人有所帮助。谢谢。
您可以在用户文档/记录的数据库中拥有“last_key_used”字段。
当用户使用 user 登录并通过时,生成一个新的随机字符串,将其存储在 last_key_used 字段中,并在签署令牌时将其添加到有效负载中。
当用户使用令牌登录时,检查数据库中的 last_key_used 以匹配令牌中的那个。
然后,例如,当用户注销时,或者如果您想使令牌无效,只需将“last_key_used”字段更改为另一个随机值,任何后续检查都将失败,从而迫使用户使用用户登录并再次通过。
保持这样的内存列表
user_id revoke_tokens_issued_before
-------------------------------------
123 2018-07-02T15:55:33
567 2018-07-01T12:34:21
如果您的令牌在一周内到期,则清除或忽略比这更早的记录。也只保留每个用户的最新记录。列表的大小将取决于您保留代币的时间以及用户撤销其代币的频率。仅在表更改时使用 db。应用程序启动时将表加载到内存中。
每个用户唯一的字符串,以及一起散列的全局字符串
这是一个例子:
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 位机密)
为什么不直接使用 jti 声明(nonce)并将其作为用户记录字段存储在列表中(依赖于 db,但至少可以使用逗号分隔的列表)?无需单独查找,正如其他人所指出的那样,您可能还是想获取用户记录,这样您就可以为不同的客户端实例拥有多个有效令牌(“到处注销”可以将列表重置为空)
聚会迟到了,经过一番研究,下面给出了我的两分钱。在注销期间,请确保正在发生以下事情...
清除客户端存储/会话
每当登录或注销分别发生时,更新用户表上次登录日期时间和注销日期时间。因此登录日期时间应始终大于注销(如果当前状态为登录且尚未注销,则保持注销日期为空)
这比保留额外的黑名单表和定期清除要简单得多。多设备支持需要额外的表格来保持登录、注销日期以及一些额外的细节,如操作系统或客户端细节。
我是通过以下方式做到的:
生成唯一的哈希,然后将其存储在 redis 和您的 JWT 中。这可以称为会话我们还将存储特定 JWT 发出的请求数 - 每次将 jwt 发送到服务器时,我们都会增加请求整数。 (这是可选的)
因此,当用户登录时,会创建一个唯一的哈希值,存储在 redis 中并注入到您的 JWT 中。
当用户尝试访问受保护的端点时,您将从 JWT 中获取唯一的会话哈希,查询 redis 并查看它是否匹配!
我们可以从这里扩展并使我们的 JWT 更加安全,方法如下:
每个特定 JWT 发出的 X 请求,我们都会生成一个新的唯一会话,将其存储在我们的 JWT 中,然后将前一个会话列入黑名单。
这意味着 JWT 不断变化并阻止陈旧的 JWT 被黑客攻击、被盗或其他原因。
aud
和 jti
声明,您走在正确的道路上。
为代币提供 1 天的到期时间 维护每日黑名单。将失效/注销令牌放入黑名单
对于令牌验证,首先检查令牌过期时间,如果令牌未过期,则检查黑名单。
对于长会话需求,应该有一种延长令牌到期时间的机制。
Kafka消息队列和本地黑名单
我考虑过使用像 kafka 这样的消息传递系统。让我解释:
例如,您可以拥有一个负责 login
和 logout
并生成 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 令牌具有中央数据库查找的想法。所以我在考虑另一种解决方案。
请让我知道您的想法,这是否有意义或者是否有明显的原因无法解释?
如果您希望能够撤销用户令牌,您可以跟踪数据库上所有已发布的令牌,并检查它们在类似会话的表上是否有效(存在)。缺点是您将在每次请求时访问数据库。
我没有尝试过,但我建议使用以下方法来允许令牌撤销,同时将数据库命中率降至最低 -
要降低数据库检查率,请根据某种确定性关联将所有已发布的 JWT 令牌分成 X 组(例如,按用户 ID 的第一位数字划分为 10 个组)。
每个 JWT 令牌都将保存组 ID 和创建令牌时创建的时间戳。例如,{ "group_id": 1, "timestamp": 1551861473716 }
服务器将所有组 ID 保存在内存中,每个组都有一个时间戳,指示属于该组的用户的最后一次注销事件是什么时候。例如,{ "group1": 1551861473714, "group2": 1551861487293, ... }
将检查具有较旧组时间戳的 JWT 令牌的请求的有效性(DB 命中),如果有效,将发布具有新时间戳的新 JWT 令牌以供客户端将来使用。如果令牌的组时间戳更新,我们信任 JWT(无 DB 命中)。
所以 -
如果令牌具有旧的组时间戳,我们只使用数据库验证 JWT 令牌,而未来的请求将不会得到验证,直到用户组中的某人注销。我们使用组来限制时间戳更改的数量(比如有一个用户登录和退出就像没有明天一样 - 只会影响有限数量的用户而不是所有人)我们限制组的数量以限制内存中保存的时间戳数量使令牌无效是一件轻而易举的事——只需将其从会话表中删除并为用户组生成一个新的时间戳。
如果“从所有设备注销”选项是可以接受的(在大多数情况下是这样):
将令牌版本字段添加到用户记录。
将此字段中的值添加到存储在 JWT 中的声明中。
每次用户注销时增加版本。
在验证令牌时,将其版本声明与存储在用户记录中的版本进行比较,如果不同则拒绝。
在大多数情况下,无论如何都需要进行一次数据库之旅来获取用户记录,因此这不会给验证过程增加太多开销。与维护黑名单不同,在黑名单中,由于需要使用连接或单独调用、清理旧记录等,因此数据库负载很大。
使用 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。
以下方法可以提供两全其美的解决方案:
让“立即”表示“~1 分钟”。
案例:
用户尝试成功登录: A. 在令牌中添加“发布时间”字段,并根据需要保留到期时间。 B. 存储用户密码哈希的哈希或在用户表中创建一个新字段,例如 tokenhash。将令牌哈希存储在生成的令牌中。用户访问一个url: A.如果“发出时间”在“立即”范围内,则正常处理令牌。不要更改“发布时间”。根据“立即”的持续时间,这是一个容易受到攻击的持续时间。但是像一两分钟这样的短持续时间不应该太冒险。 (这是性能和安全性之间的平衡)。三是不需要在这里打分贝。 B. 如果令牌不在“立即”范围内,请根据数据库检查令牌哈希。如果没问题,请更新“发布时间”字段。如果不行,则不处理请求(最终强制执行安全性)。用户更改令牌哈希以保护帐户。在“立即”的将来,该帐户是安全的。
我们将数据库查找保存在“立即”范围内。如果在“立即”持续时间内有来自客户端的请求突发,这是最有益的。
tokenhash
?
tokenhash
更改为特殊值。在“立即”的未来,所有其他可能的副本也会变得无效,从而强制重新登录。缺点:它会从所有设备上注销用户(还没有想到如何克服这个问题)。
不使用 JWT 的刷新...
我想到了 2 种攻击场景。一是关于登录凭据的泄露。另一个是 JWT 的实际盗窃。
对于受损的登录凭据,当发生新登录时,通常会向用户发送电子邮件通知。因此,如果客户不同意成为登录的人,则应建议他们重置凭据,这应将上次设置密码的日期时间保存到数据库/缓存中(并在用户在初始注册时设置密码)。每当授权用户操作时,应从数据库/缓存中获取用户更改密码的日期时间,并与生成给定 JWT 的日期时间进行比较,并禁止在所述日期之前生成的 JWT 的操作- 重置凭证的时间,因此基本上使此类 JWT 无用。这意味着将生成 JWT 的日期时间保存为 JWT 本身的声明。在 ASP.NET Core 中,可以使用策略/要求来进行此比较,如果失败,则禁止客户端。因此,每当完成凭据重置时,这都会在全局范围内注销后端用户。
对于 JWT 的实际盗窃...... JWT 的盗窃并不容易被发现,但过期的 JWT 很容易解决这个问题。但是如何在 JWT 过期之前阻止攻击者呢?它具有实际的全局注销。它类似于上面描述的凭据重置。为此,通常将用户启动全局注销的日期时间保存在数据库/缓存中,并在授权用户操作时,获取它并将其与给定 JWT 的生成日期时间进行比较,并禁止该操作在上述全局注销日期时间之前生成的 JWT,因此基本上使此类 JWT 无用。如前所述,这可以使用 ASP.NET Core 中的策略/要求来完成。
现在,您如何检测 JWT 被盗?我现在对此的回答是偶尔提醒用户全局注销并再次登录,因为这肯定会使攻击者注销。
使令牌无效的好方法仍然需要数据库访问。用于包括用户记录的某些部分何时更改的目的,例如更改角色、更改密码、电子邮件等。可以在用户记录中添加一个 modified
或 updated_at
字段,记录此更改的时间,然后将其包含在声明中。因此,当 JWT 被验证时,您将声明中的时间与数据库中记录的时间进行比较,如果声明的时间在之前,则令牌无效。这种方法也类似于将 iat
存储在数据库中。
注意:如果您使用的是 modified
或 updated_at
选项,那么您还必须在用户登录和注销时对其进行更新。
只需创建将以下对象添加到您的用户架构:
const userSchema = new mongoose.Schema({
{
... your schema code,
destroyAnyJWTbefore: Date
}
并且每当您在 /login 上收到 POST 请求时,将此文档的日期更改为 Date.now()
最后,在您的身份验证检查代码中,即在您检查 isAuthanticated
或 protected
或您使用的任何名称的中间件中,只需添加一个验证以检查 myjwt.iat
是否大于 userDoc.destroyAnyJWTbefore
。
如果您想在服务器端销毁 JWT,则此解决方案在安全性方面是最好的。
该解决方案不再依赖于客户端,它打破了使用 JWT 的主要目标,即停止在服务器端存储令牌。
这取决于您的项目上下文,但很可能您希望从服务器销毁 JWT。
如果您只想从客户端销毁令牌,只需从浏览器中删除 cookie(如果您的客户端是浏览器),同样可以在智能手机或任何其他客户端上完成。
如果选择从服务器端销毁令牌,我建议您使用 Radis 快速执行此操作,通过实现其他用户提到的黑名单样式。
现在的主要问题是:JWT 没用吗?天知道。
我将回答如果我们在使用 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。
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
另一种方法是为关键 API 端点提供一个中间件脚本。如果令牌被管理员无效,此中间件脚本将检查数据库。对于不需要立即完全阻止用户访问的情况,此解决方案可能很有用。
在此示例中,我假设最终用户也有一个帐户。如果不是这种情况,那么该方法的其余部分不太可能奏效。
当您创建 JWT 时,将其保存在与正在登录的帐户关联的数据库中。这确实意味着您可以仅从 JWT 中提取有关用户的其他信息,因此取决于环境,这可能会也可能不会没事。
在之后的每个请求中,您不仅执行(我希望)您使用的任何框架附带的标准验证(验证 JWT 是否有效),它还包括用户 ID 或其他令牌(需要匹配数据库中的那个)。
当您注销时,删除 cookie(如果使用)并使数据库中的 JWT(字符串)无效。如果无法从客户端删除 cookie,那么至少注销过程将确保令牌被销毁。
我发现这种方法,再加上另一个唯一标识符(因此数据库中有 2 个持久项并且可供前端使用),会话非常有弹性
以下是无需在每个请求上调用数据库的方法:
将有效令牌的哈希图保存在内存缓存中(例如,大小有限的 LRU)
检查令牌时:如果令牌在缓存中,立即返回结果,不需要数据库查询(大多数情况)。否则执行全面检查(查询数据库,检查用户状态和无效令牌...)。然后更新缓存。
使令牌无效时:将其添加到数据库中的黑名单中,然后更新缓存,如果需要,向所有服务器发送信号。
请记住,缓存应该具有有限的大小,例如 LRU,否则您可能会耗尽内存。
即使您将令牌从存储中删除,它仍然有效,但仅在短时间内有效,以减少被恶意使用的可能性。
您可以创建一个 deny-listing
,从存储中删除令牌后,您可以将令牌添加到此列表中。如果您有一个微服务服务,则使用此令牌的所有其他服务都必须添加额外的逻辑来检查此列表。这将集中您的身份验证,因为每个服务器都必须检查一个集中的数据结构。
如果不对每个令牌验证进行数据库查找,这似乎真的很难解决。我能想到的替代方法是在服务器端保留一个无效令牌的黑名单;每当发生更改时,都应该在数据库上进行更新,以便在重新启动时保持更改,方法是让服务器在重新启动时检查数据库以加载当前的黑名单。
但是,如果您将它保存在服务器内存中(某种全局变量),那么如果您使用多个服务器,它将无法跨多个服务器进行扩展,因此在这种情况下,您可以将其保存在共享的 Redis 缓存中,这应该是设置以将数据保存在某处(数据库?文件系统?)以防它必须重新启动,并且每次启动新服务器时,它都必须订阅 Redis 缓存。
作为黑名单的替代方案,使用相同的解决方案,您可以使用每个会话保存在 redis 中的哈希来执行此操作,正如另一个 answer 指出的那样(但不确定对于许多用户登录会更有效)。
听起来是不是很复杂?它对我有用!
免责声明:我没有使用过 Redis。
2)
的详细版本。虽然它工作得很好,但我个人认为与传统的会话存储没有太大区别。我想存储要求会更低,但你仍然需要一个数据库。 JWT 对我最大的吸引力是完全不使用数据库进行会话。