无效的JSON Web令牌


420

对于我正在研究的一个新的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数据库。

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


1
如果您使用的是'express-jwt'软件包,则可以查看该isRevoked选项,或尝试复制相同的功能。github.com/auth0/express-jwt#revoked-tokens
Signus 2015年

1
考虑对访问令牌使用较短的到期时间,并使用具有较长到期期限的刷新令牌,以允许检查数据库中用户的访问状态(列入黑名单)。auth0.com/blog/…–罗门
Rohmer)

另一个选择是在生成jwt令牌的同时将IP地址附加到有效负载中,并检查存储的IP与传入请求是否相同的IP地址。例如:nodeJs中的req.connection.remoteAddress。有些ISP提供商不会为每个客户发布静态IP,我认为除非客户端重新连接到互联网,否则这不会成为问题。
Gihan Sandaru

Answers:


391

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

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中轻松使用它,然后延迟会大大降低。
亚伦·瓦格纳

2
这段编码恐怖的帖子提供了一些建议:使会话承载Cookie(或令牌)的时间较短,但使用户看不见它-似乎与#3一致。我自己的直觉(也许是因为它更传统)只是让令牌(或它的哈希)充当白名单会话数据库的键(类似于#2)
funseiki 2014年

7
这篇文章写得很好,是2)上述内容的详尽版本。虽然效果很好,但我个人认为与传统的会话存储没有太大区别。我想存储需求会更低,但是您仍然需要数据库。JWT对我来说最大的吸引力是完全不使用数据库进行会话。
Matt Way

210
当用户更改密码时,使令牌失效的一种常见方法是使用其密码的哈希值对令牌进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。您可以通过在用户记录中包含上次注销时间,并结合使用上次注销时间和密码哈希对令牌进行签名来将其扩展到注销。每次您需要验证令牌签名时,都需要进行一次数据库查找,但大概还是要查找用户。
特拉维斯·特里

4
通过将黑名单保存在内存中,可以使黑名单变得高效,因此仅需命中数据库即可记录无效记录并删除过期的无效记录,并且只能在服务器启动时进行读取。在负载平衡架构下,内存中的黑名单可以在较短的间隔(例如10秒)内轮询数据库,从而限制了无效令牌的暴露。这些方法允许服务器继续验证请求,而无需按请求访问数据库。
乔·拉普

85

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

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

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

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


9
我认为这种方法并不理想。尽管它确实有效并且很简单,但请设想一下您使用公共密钥的情况-您不想在想要使单个令牌失效的任何时候去重新创建该密钥。
Signus 2015年

1
@KijanaWoodard,一个公钥/私钥对可以用来验证签名,从而有效地将其作为RS256算法中的秘密。在此处显示的示例中,他提到更改机密以使JWT无效。这可以通过以下两种方法来完成:a)引入与签名不匹配的假公钥,或b)生成新的公钥。在这种情况下,它不理想。
Signus 2015年

1
@Signus-陷阱 并非使用公钥作为秘密,而是其他人可能依靠公钥来验证签名。
Kijana Woodard

8
这是非常糟糕的解决方案。使用JWT的主要原因是它是无状态且可扩展的。使用动态秘密会引入状态。如果服务跨多个节点群集,则每次发出新令牌时都必须同步密钥。您将必须将机密存储在数据库或其他外部服务中,这仅仅是重新发明基于cookie的身份验证
Tuomas Toivonen

5
@TuomasToivonen,但是您必须使用秘密对JWT进行签名,并且能够使用相同的秘密来验证JWT。因此,您必须将机密存储在受保护的资源上。如果机密信息遭到泄露,则必须对其进行更改,并将此更改分发给每个节点。具有群集/扩展功能的托管服务提供商通常允许您将机密存储在其服务中,以使分发这些机密变得容易且可靠。
罗默

67

这主要是一篇很长的评论,以@mattway的回答为依据并得到支持

鉴于:

此页面上其他一些建议的解决方案主张在每次请求时都访问数据存储。如果您访问主数据存储区以验证每个身份验证请求,那么我会发现使用JWT代替其他已建立的令牌身份验证机制的理由更少。从本质上讲,您已使JWT成为有状态的,而不是使每次访问数据存储都成为无状态的。

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

鉴于:

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

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

用户密码已更改。

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

用户由管理员注销。

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

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

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

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

对于通过初始JWT身份验证的每个身份验证请求,您仍然必须对内存数据库进行调用,但是不必在其中存储整个用户组的密钥。(对于给定的站点,这可能不重要)。


15
我不同意你的回答。击中数据库不会使任何状态保持不变。在后端存储状态。没有创建JWT,因此您不必在每次请求时都访问数据库。使用JWT的每个主要应用程序都由数据库支持。JWT解决了一个完全不同的问题。en.wikipedia.org/wiki/Stateless_protocol
朱利安

6
@Julian您能详细说明一下吗?那么,JWT真正解决了哪个问题?
zero01alpha

8
@ zero01alpha身份验证:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。信息交换:JSON Web令牌是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名,所以您可以确保发件人是他们所说的人。参见jwt.io/introduction
朱利安(Julian)

7
@Julian我不同意您的不同意见:) JWT解决了(服务方面的)问题,需要访问一个集中式实体,该实体为任何给定的客户提供授权信息。因此,与服务A和服务B相比,服务A和B不必访问某些资源来确定客户端X是否具有执行某项操作的权限,而是从X接收一个证明其权限的令牌(通常由第三者发布)派对)。无论如何,JWT是一种工具,可帮助避免系统中服务之间的共享状态,尤其是当它们由多个服务提供商控制时。
LIvanov '18

1
同样来自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.
Giantas,

43

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

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

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


15
这是一个有趣的想法,唯一的事情就是将版本存储在哪里,因为令牌的目的之一是它是无状态的,不需要使用数据库。硬编码的版本将很难修改,而数据库中的版本号将抵消使用令牌的某些好处。
史蒂芬·史密斯

13
大概您已经在令牌中存储了一个用户ID,然后查询数据库以检查该用户是否存在/是否有权访问api端点。因此,通过将jwt令牌版本号与用户上的jwt令牌版本号进行比较,您无需执行任何额外的数据库查询。
DaftMonk

5
我不应该这么说,因为在很多情况下,您可能将令牌与完全不涉及数据库的验证一起使用。但是我认为在这种情况下很难避免。
DaftMonk

11
如果用户从多个设备登录怎么办?应该对所有令牌使用一个令牌,还是应该使先前的令牌无效?
meeDamian 2014年

10
我同意@SergioCorrea,这将使JWT与其他任何令牌身份验证机制一样具有状态。
Ed J

40

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

目标:

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

解决方案:

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

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

缺点:

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

优点:

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

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


如果一个人从一台计算机起床,让另一个人使用同一台计算机,该方案如何?第一个人将注销,并期望注销立即阻止第二个人。如果第二个人是普通用户,则客户端可以通过删除令牌轻松地阻止该用户。但是,如果第二个用户具有黑客技能,则该用户有时间恢复仍然有效的令牌以进行身份​​验证,成为第一个用户。似乎没有办法避免立即使令牌无效而没有延迟。
乔·拉普

5
或者,您可以从会话/本地存储或Cookie中删除您的JWT。
卡米尔·基茨屈斯基

1
谢谢@Ashtonian。经过大量研究,我放弃了JWT。除非您花了很长的时间来保护密钥,或者除非您委托给安全的OAuth实现,否则JWT比常规会话更容易受到攻击。请参阅我的完整报告:by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html
Joe Lapp

2
使用刷新令牌是允许列入黑名单的关键。很棒的解释:auth0.com/blog/…–罗门
Rohmer)

1
在我看来,这是最好的答案,因为它结合了一个短期访问令牌和一个可以被列入黑名单的长期刷新令牌。注销时,客户端应删除访问令牌,以便第二个用户无法访问(即使访问令牌在注销后仍保持有效几分钟)。@Joe Lapp说,即使被删除,黑客(第二用户)也会获得访问令牌。怎么样?
M3RS

14

我一直在考虑的一种方法是iat在JWT中始终具有一个值(发布于)。然后,当用户注销时,将该时间戳记存储在用户记录中。验证JWT时,只需将iat和上次注销的时间戳进行比较即可。如果iat年龄较大,则无效。是的,您必须去数据库,但是如果JWT有效,无论如何我都会一直拉用户记录。

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

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


1
好主意!解决“一个设备”的问题是使它成为一种应急功能,而不是注销。将日期存储在用户记录中,该日期会使在其之前发出的所有令牌失效。诸如之类的token_valid_after东西。太棒了!
OneHoopyFrood

1
嘿@OneHoopyFrood您有一个示例代码可以帮助我更好地理解该想法?非常感谢您的帮助!
alexventuraio '16

2
像所有其他建议的解决方案一样,该解决方案也需要数据库查找,这就是存在此问题的原因,因为避免查找在这里是最重要的!(性能,可伸缩性)。通常情况下,您不需要数据库查找即可拥有用户数据,而您已经从客户端获取了该数据。
罗布·埃文斯

9

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

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


1
您如何拒绝令牌?您可以显示一个简短的示例代码吗?
alexventuraio '16

1
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
Vanuan

15
需要数据库查找!
罗布·埃文斯

5

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

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

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

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


这是我一直在考虑的解决方案,但是它具有以下缺点:(1)您正在对每个请求进行数据库查找以检查随机性(取消使用令牌而不是会话的原因)或仅在刷新令牌过期后才进行间歇检查(防止用户立即注销或会话立即终止);(2)注销使用户从所有浏览器和所有设备注销(这不是传统上的预期行为)。
乔·拉普

您无需在用户注销时更改密钥,只需在用户更改密码或(如果您提供密码时)-当他们选择从所有设备注销时即可
NickVarcha

3

保留这样的内存列表

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

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


2
大多数生产站点都在多台服务器上运行,因此该解决方案将无法使用。添加Redis或类似的内存缓存会使系统复杂化,并且通常会带来比解决方案更多的问题。
user2555515

@ user2555515所有服务器都可以与数据库同步。您可以选择是否每次都访问数据库。您可以判断出它带来了什么问题。
Eduardo

3

------------------------回答这个问题有点晚了,但是可能对某人有帮助------------- -----------

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

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

JWT软件包的问题在于它没有提供任何破坏令牌的方法或方式。关于JWT,您可以使用上述不同的方法。但是在这里,我选择了jwt-redis。

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

这个库(jwt-redis)完全重复了库jsonwebtoken的全部功能,但有一个重要的补充。Jwt-redis允许您将令牌标签存储在redis中以验证有效性。Redis中没有令牌标签会使令牌无效。要销毁jwt-redis中的令牌,有一种destroy方法

它以这种方式工作:

1)从npm安装jwt-redis

2)创建-

var redis = require('redis');
var JWTR =  require('jwt-redis').default;
var redisClient = redis.createClient();
var jwtr = new JWTR(redisClient);

jwtr.sign(payload, secret)
    .then((token)=>{
            // your code
    })
    .catch((error)=>{
            // error handling
    });

3)验证 -

jwtr.verify(token, secret);

4)摧毁 -

jwtr.destroy(token)

注意:您可以在令牌登录期间提供expiresIn,方法与JWT中提供的相同。

可能对某人有帮助


2

为什么不只使用jti声明(立即)并将其作为用户记录字段存储在列表中(取决于数据库,但至少用逗号分隔的列表就可以了)?无需单独查找,就像其他人指出的那样,无论如何您都想获取用户记录,因此您可以为不同的客户端实例使用多个有效令牌(“注销到处”可以将列表重置为空)


是的,这个。也许在用户表和新的(会话)表之间建立一对多关系,因此您可以将元数据与jti声明一起存储。
彼得·拉达

2
  1. 给出令牌的1天到期时间
  2. 维持每日黑名单。
  3. 将无效/注销令牌放入黑名单

对于令牌验证,如果令牌未过期,请先检查令牌的到期时间,然后再检查黑名单。

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


4
将令牌放入黑名单,您的无国籍状态就会消失
KeremBaydoğan17年

2

晚了晚会,经过一些研究,下面给了我两美分。注销期间,请确保正在发生以下事情...

清除客户端存储/会话

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

这比保持额外的黑名单表和定期清除要简单得多。多设备支持需要附加表来保持登录状态,登出日期以及一些其他详细信息,例如操作系统或客户端详细信息。


2

每个用户字符串唯一,并且全局字符串散列在一起

作为JWT机密部分,允许单独和全局令牌无效。在请求身份验证期间以数据库查找/读取为代价的最大灵活性。由于它们很少更改,因此也易于缓存。

这是一个例子:

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位机密)


1
一些更多的细节就足够了
巨人

2
@giantas,我认为Mark的意思是签名部分。因此,与其仅使用单个密钥对JWT签名,不如将其组合为每个客户端唯一的密钥。因此,如果要使用户的所有会话无效,只需更改该用户的密钥,如果要使系统中的所有会话无效,只需更改该全局单个密钥。
Tommy Aria Pradana

1

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

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

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

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

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

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

这意味着JWT不断变化,并停止了过时的JWT被黑客入侵,被盗等。


1
您可以对令牌本身进行哈希处理并将该值存储在redis中,而不是将新的哈希值注入令牌中。
弗鲁格

还要在JWT中签出audjti声明,您在正确的道路上。
彼得·拉达

1

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

我没有尝试过,但是我建议使用以下方法允许令牌吊销,同时将数据库命中率保持在最低水平-

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

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

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

具有JWT令牌且组时间戳较旧的请求将进行有效性检查(数据库命中),如果有效,则将发出具有新时间戳的新JWT令牌供客户将来使用。如果令牌的组时间戳是新的,则我们信任JWT(无数据库命中)。

所以-

  1. 仅当令牌具有旧的组时间戳时,我们才使用DB验证JWT令牌,而将来的请求只有在用户组中的某人注销后才会得到验证。
  2. 我们使用组来限制时间戳更改的次数(例如,有用户登录和退出,就像没有明天一样-仅会影响有限数量的用户,而不是每个用户)
  3. 我们限制组的数量以限制保存在内存中的时间戳数量
  4. 使令牌失效很容易-只需将其从会话表中删除并为用户组生成新的时间戳即可。

相同的列表可以保存在内存中(适用于C#应用程序),这样就无需为每个请求都命中数据库。该列表可以在应用程序启动时从db加载
dvdmn

1

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

  • 将令牌版本字段添加到用户记录。
  • 将此字段中的值添加到JWT中存储的声明中。
  • 每次用户注销时增加版本。
  • 验证令牌时,将其版本声明与存储在用户记录中的版本进行比较,如果不相同,则拒绝。

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


0

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

  1. LastValidTime(默认值:创建时间)
  2. 已登录(默认:true)

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

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

  1. JWT有效吗
  2. JWT有效负载创建时间是否大于User LastValidTime
  3. 用户是否已登录

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

用户X有两个设备A,B。他在晚上7点使用设备A和设备B登录到我们的服务器。(假设JWT的到期时间为12小时)。A和B都具有创建时间为7pm的JWT

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

在9:30,Thief先生尝试使用设备B登录。即使Logged-In为false,我们也会检查数据库,因此我们将不允许。

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

在晚上10:30,Thief先生尝试登录。即使Logged-In为true,也是如此。数据库中的LastValidTime是9 pm,但是B的JWT已将时间创建为7 pm。因此,将不允许他访问该服务。因此,使用没有密码的设备B后,一台设备注销后,他将无法使用已创建的JWT。


0

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

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

如果只想注销一个useragent(或一个用户),则也可以调用一个端点(这将使令牌无效)。同样,对于Keycloak,依赖方只需要呼叫端点

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

如果您想了解更多,请链接


-1

如果没有DB查找每个令牌验证,这似乎很难解决。我可以想到的替代方法是在服务器端保留一个无效令牌的黑名单。每当发生更改以在重新启动期间持久保留更改时,都应在数据库上更新该更新,方法是使服务器在重新启动时检查数据库以加载当前黑名单。

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

替代黑名单,使用相同的解决方案,您可以使用每个会话在redis中保存的哈希值来实现此目的,这是其他答案所指出的(但不确定如果许多用户登录,这样做会更有效)。

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

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


-1

如果您使用axios或类似的基于Promise的http请求库,则可以简单地破坏.then()部件内部前端的令牌。用户执行此功能后,它将在response .then()部分中启动(来自服务器端点的结果代码必须正确,200)。用户在搜索数据时单击此路由后,如果数据库字段user_enabled为false,它将触发销毁令牌,并且用户将立即注销并停止访问受保护的路由/页面。当用户永久登录时,我们不必等待令牌过期。

function searchForData() {   // front-end js function, user searches for the data
    // protected route, token that is sent along http request for verification
    var validToken = 'Bearer ' + whereYouStoredToken; // token stored in the browser 

    // route will trigger destroying token when user clicks and executes this func
    axios.post('/my-data', {headers: {'Authorization': validToken}})
     .then((response) => {
   // If Admin set user_enabled in the db as false, we destroy token in the browser localStorage
       if (response.data.user_enabled === false) {  // user_enabled is field in the db
           window.localStorage.clear();  // we destroy token and other credentials
       }  
    });
     .catch((e) => {
       console.log(e);
    });
}

-3

我只是将令牌保存到用户表中,当用户登录时,我将更新新令牌,并且当auth等于用户当前的jwt时。

我认为这不是最佳解决方案,但对我有用。


2
当然不是最好的!有权访问数据库的任何人都可以轻松模拟任何用户。
user2555515 '19

1
@ user2555515如果对存储在数据库中的令牌进行了加密(就像应该对存储在数据库中的任何密码进行加密)一样,此解决方案也可以正常工作。Stateless JWT和之间存在差异Stateful JWT(与会话非常相似)。 Stateful JWT可以通过维护令牌白名单而受益。
TheDarkIn1978 '19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.