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

该项目是一个利用socket.io的游戏-使用基于令牌的会话将非常有用在一个会话中将有多个通信通道(web和socket.io)的情况下,如何使用jwt方法从服务器提供令牌/会话无效?

我还想了解我应该用这种范式寻找哪些常见(或不常见)的陷阱/攻击。例如,如果此范例易受与基于会话存储/ Cookie的方法相同/不同类型的攻击的影响。 br />
会话存储登录:

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 / ...

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

我倾向于说正是这个问题使JWT不太有用。这是一个很酷的解决方案,也是一种安全地打破“永远不信任客户”规则的优雅方法,但是最终解决了它的问题(主要是无效问题)总是不可避免地重新引入了状态

#1 楼

我也一直在研究这个问题,尽管以下所有想法都不是完整的解决方案,但它们可能会帮助其他人排除想法或提供进一步的想法。
1)只需从客户端中删除令牌
这对服务器端的安全性没有任何帮助,但是它确实通过删除令牌而阻止了攻击者(即,在注销之前,他们必须先窃取了令牌)。
2)创建一个令牌阻止列表
您可以存储无效令牌,直到它们的初始到期日期为止,然后将它们与传入请求进行比较。但是,这似乎可以消除完全基于令牌的原因,因为您将需要为每个请求触摸数据库。不过,存储空间可能会更小,因为您只需要存储注销和到期时间之间的令牌(这是一种直觉,并且绝对取决于上下文)。
3)只保留令牌到期时间缩短并经常轮换它们
如果您将令牌的到期时间保持在足够短的时间间隔内,并让运行中的客户端跟踪并在必要时请求更新,则数字1可以有效地用作完整的注销系统。这种方法的问题在于,它使得无法在关闭客户端代码之间保持用户登录状态(取决于您设置的到期间隔时间)。
应急计划
紧急情况或用户令牌遭到破坏时,您可以做的一件事是允许用户使用其登录凭据更改基础用户查找ID。这将使所有关联的令牌无效,因为将不再能够找到关联的用户。
我还想指出,最好将上次登录日期与令牌一起包含在内,这样您就可以能够在很长一段时间后强制重新登录。
关于使用令牌进行攻击的相似性/差异性,本文讨论了以下问题:https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies -vs-token.markdown

评论


优秀的方法。我的直觉是将所有这三个方法组合在一起,和/或在每“ n”个请求之后(而不是计时器)请求一个新的令牌。我们正在使用redis进行内存中的对象存储,我们可以在情况2中轻松使用它,然后延迟会大大降低。

–亚伦·瓦格纳(Aaron Wagner)
2014年4月18日14:04



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

– Matt Way
2014年7月31日下午3:18

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

–特拉维斯·特里(Travis Terry)
2015年2月6日在1:02



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

–乔·拉普(Joe Lapp)
16年5月22日在5:48

@TravisTerry您的方法在单片应用程序中很有用,但要在微服务应用程序中实现,您需要所有服务来存储用户的密码和最后一次登录或提出获取请求的服务,但这两种方法都是不好的主意。

– adnanmuttaleb
19年8月5日在5:59

#2 楼

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

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

它不会不需要对实际令牌内容(或查找ID)进行任何修改。必需的(例如较短的令牌到期时间或使令牌中存储的密钥无效)。

评论


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

–Signus
2015年5月30日,0:10

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

–Signus
2015年9月30日15:09

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

–吉嘉娜·伍德德(Kijana Woodard)
2015年9月30日在20:01



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

– Tuomas Toivonen
17年5月5日在13:29

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

–罗门
17年8月17日在19:43

#3 楼

这主要是一条长长的评论,以支持@mattway的答案并以此为基础。给出以下内容:此页面上的其他一些建议的解决方案主张在每次请求时都击中数据存储区。如果您访问主数据存储区以验证每个身份验证请求,那么我会发现使用JWT代替其他已建立的令牌身份验证机制的理由更少。本质上,您已使JWT成为有状态的,而不是每次都访问数据存储都变为无状态。可能还有其他用例。)

给出:

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

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

用户的密码已更改。

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

用户已被管理员注销。

站点更改了JWT令牌中的任何其他应用程序关键数据管理员。在这些情况下,您不能等待令牌到期。令牌失效必须立即发生。另外,您不能相信客户不要保留并使用旧令牌的副本,无论是否出于恶意目的。

因此:
我认为@ matt-way的回答是, #2 TokenBlackList是将所需状态添加到基于JWT的身份验证的最有效方法。与用户总数相比,令牌列表将非常小,因为它只需要保留列入黑名单的令牌,直到令牌到期为止。我可以通过将无效令牌放置在redis,memcached或另一个支持在密钥中设置过期时间的内存数据存储中来实现。

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

评论


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

–朱利安
16年5月17日在18:06

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

– zero01alpha
17-10-5在18:02

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

–朱利安
17-10-5在18:16

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

– LIvanov
18年4月19日在14:36



如果每次都命中DB,则会失去具有刷新令牌的目的。

– Koushik Shom Choudhury
2月24日7:57

#4 楼

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

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

评论


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

–史蒂芬·史密斯(Stephen Smith)
14年7月6日在21:17

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

– DaftMonk
2014年7月7日在5:58



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

– DaftMonk
2014年7月7日在6:16

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

–meeDamian
2014年8月11日在2:38

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

– Ed J
16年4月27日在8:56

#5 楼

还没有尝试过,它基于其他一些答案使用了大量信息。这里的复杂性是避免对用户信息请求的每次服务器端数据存储调用。大多数其他解决方案都需要对用户会话存储区的每个请求进行数据库查找。在某些情况下可以这样做,但这是为了避免此类调用并使所需的服务器端状态变得很小而创建的。您最终将重新创建服务器端会话,但是会话会话的规模很小,无法提供所有强制失效功能。但是,如果要执行此操作,请遵循以下要点:目标:减少数据存储的使用(无状态)。
可以强制注销所有用户。
可以随时强制退出任何个人的功能。
在一定时间后要求重新输入密码的功能。
可以与多个客户端一起使用的功能。当用户单击特定客户端的注销时,强制重新登录。 (为防止有人在用户离开后“删除”客户令牌,请参阅注释以获取更多信息)

解决方案:


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

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

缺点:


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

优点:


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

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

评论


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

–乔·拉普(Joe Lapp)
16-5-22在3:49



或者,您可以从会话/本地存储或cookie中删除JWT。

–卡米尔(KamilKiełczewski)
16年7月11日在22:33

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

–乔·拉普(Joe Lapp)
17年1月11日,下午1:42

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

–罗门
17年8月17日在19:56

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

– M3RS
18/12/6在13:19

#6 楼

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

我看到的主要缺点是它将记录它们如果它们是在多个浏览器中,或者也有移动客户端,则无需进行所有会话。部分检查可能与最后一次有效的iat时间的全局时间戳相对。

评论


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

– OneHoopyFrood
2015年11月4日在18:12

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

–Rob Evans
18年4月26日在10:12

这可以轻松地与不同答案中建议的刷新令牌方法结合使用,以减少对数据存储的访问次数。

–IMSoP
7月4日13:55

#7 楼

我来晚了一点,但是我想我有一个不错的解决方法。我还将发布的日期/时间存储在JWT中。验证令牌时,我检查密码在颁发令牌后是否已更改,并且即使令牌尚未过期也被拒绝。

评论


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

–alexventuraio
16年1月9日在23:39

如果(jwt.issue_date
–万安
16 Jan 25 '21:07



需要数据库查询!

–Rob Evans
18年4月26日在10:12

#8 楼

------------------------回答这个问题有点晚了,但也许对某人会有帮助-----------

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

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

JWT软件包的问题在于它不提供销毁令牌的任何方法或方法。关于上文提到的JWT。但是在这里,我使用了jwt-redis。 -redis)完全重复了库jsonwebtoken的全部功能,但有一个重要的补充。 Jwt-redis允许您将令牌标签存储在redis中以验证有效性。 Redis中没有令牌标签会使令牌无效。要销毁jwt-redis中的令牌,有一种销毁方法

它以这种方式工作:

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)


注意:您可以在令牌登录期间像JWT中提供的一样提供expiresIn。帮助某人

#9 楼

您可以在数据库的用户文档/记录上有一个“ last_key_used”字段。在对令牌进行签名时将其添加到有效负载中。

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

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

评论


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

–乔·拉普(Joe Lapp)
16年5月22日在3:56

当用户注销时,您无需更改密钥,仅在用户更改密码时,或者-如果您提供密码,则-当用户选择从所有设备注销时

– NickVarcha
16年7月15日在2:05

#10 楼

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

评论


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

– Peter Lada
19年7月24日在8:27

#11 楼

保持这样的内存列表

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


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

评论


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

–user2555515
19年3月21日在15:54

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

–爱德华多
19年4月4日在23:42

#12 楼


每个用户字符串都是唯一的,并且将全局字符串哈希在一起作为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位机密)

评论


一些更多的细节就足够了

– giantas
19/12/14在15:18

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

–汤米·阿里亚·普拉达纳(Tommy Aria Pradana)
4月3日15:39

#13 楼


为令牌提供1天的到期时间
维护每日黑名单。
将无效的/注销的令牌放入黑名单中。如果令牌未过期,则先过期,然后再将其列入黑名单。

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

评论


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

– KeremBaydoğan
17年3月21日在17:37

如果您在数据库中维护了后备列表,那么您仍然会保持无状态

– Ebru Yener
7月28日8:49

将黑名单放入数据库中,即可提高性能和可伸缩性。

– KeremBaydoğan
7月28日晚上11:58

#14 楼

晚会之后,经过一些研究,下面给出了我的两分钱。
在注销期间,请确保正在发生以下事情... br />分别在发生登录或注销时更新用户表的上次登录日期时间和注销日期时间。因此,登录日期时间应始终大于注销时间(如果当前状态为登录状态且尚未注销,则注销日期时间为null)

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

#15 楼

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


生成unique hash,然后将其存储在redis和您的JWT中。这可以称为会话



我们还将存储特定JWT发出的请求数-每次将jwt发送到服务器时,我们都会增加请求整数。 (这是可选的)




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

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

我们可以扩展此范围,并使我们的JWT更加安全,方法如下:

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

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

评论


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

–节俭
17年3月14日在16:29

还要检查JWT中的aud和jti声明,您的方法正确。

– Peter Lada
19年7月24日在8:25

#16 楼

如果要撤消用户令牌,可以跟踪数据库中所有已颁发的令牌,并检查它们是否在会话表中有效(存在)。
缺点是您将在每个请求上都命中数据库。

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

数据库检查费率,根据确定性关联将所有已发行的JWT令牌划分为X组(例如,由用户ID的第一位数字组成10组)。创建令牌时创建的时间戳。例如,{ "group_id": 1, "timestamp": 1551861473716 }

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

带有JWT令牌且组时间戳较旧的请求将被检查有效性(数据库命中),如果有效,则将发出具有新时间戳的新JWT令牌以供客户将来使用。
如果令牌的组时间戳是新的,则我们信任JWT(无数据库命中)。如果令牌具有旧的组时间戳,则使用数据库,而将来的请求只有在用户组中的某人退出后才会得到验证。
我们使用组来限制时间戳更改的次数(例如,有一个用户像没有明天一样登录和注销-仅会影响有限数量的用户,而不是每个用户)
我们限制了组的数量,以限制保存在内存中的时间戳数量。只需将其从会话表中删除,并为用户组生成新的时间戳即可。


评论


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

–dvdmn
19 Mar 25 '19在14:59

#17 楼

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


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

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

#18 楼

Kafka消息队列和本地黑名单
我考虑过使用像kafka这样的消息传递系统。让我解释一下:
例如,您可以拥有一个微服务(称为userMgmtMs服务),该微服务负责loginlogout并产生JWT令牌。然后,此令牌将传递给客户端。
现在,客户端可以使用此令牌来调用不同的微服务(将其称为pricesMs),在priceMs内将没有数据库检查到users表,从中初始创建令牌被触发了。该数据库仅存在于userMgmtM中。此外,JWT令牌还应包含权限/角色,以便priceMs无需从数据库中查找任何内容即可使弹簧安全性正常工作。由JWT令牌中提供的数据创建(显然没有密码)。
那么,如何注销或使令牌无效?由于我们不想在每次请求priecesMs时都调用userMgmtMs数据库(这会引入很多不必要的依赖关系),因此解决方案是使用此令牌黑名单。
而不是将黑名单保留在中心并具有依赖关系在所有微服务的一个表上,我建议使用kafka消息队列。此外,它将带有此令牌内容的kafka事件发送到预订了所有其他微服务的内部kafka服务。
一旦其他微服务收到kafka事件,它们也会将其也放置在其内部黑名单中。
即使某些微服务在注销时已关闭,它们最终仍将再次启动,并在以后的状态下接收消息。
由于kafka是经过开发的,因此客户可以参考自己阅读过的消息,因此可以确保没有任何客户,不管是up还是up,都不会错过任何这种无效令牌。
我唯一想到的唯一问题是kafka消息传递服务将再次引入单点故障。但这是相反的,因为如果我们有一个全局表,其中保存了所有无效的JWT令牌,并且此数据库或微服务已关闭,则无效。使用kafka方法+对于普通用户注销,客户端删除JWT令牌后,在大多数情况下,kafka的停机时间甚至不会引起注意。由于黑名单是作为内部副本在所有微服务之间分发的。在这种情况下,将秘密作为最后的手段可以有所帮助。还是只是在执行此操作之前确保kafka已经启动。所以我在考虑另一种解决方案。
请让我知道您的想法,这有意义还是有明显的原因使它无法解决?

评论


我同意你的做法。当我们谈论可能性时,MQ比我们想象的要稳定得多。

–矩阵
8月7日下午3:46

我为这个想法完成了一条快乐的路,并且看起来效果很好。微服务可以完全独立,并且仅对照其黑名单的本地副本进行检查。如果它们掉线了,它们将在再次上升时收到所有消息,因此不会丢失消息。假设MQ中没有故障。现在,我正在研究如何进行CSRF保护的问题,不确定如何进行。

–华伦丁
8月7日10:33

我刚刚将带有此信息的GrantAuthorities和用户ID添加到JWT,就可以像在微服务(antMatchers等)中使用真实用户数据库一样使用WebSecurityConfigurerAdapter。如果用户角色和权限发生更改,则还需要向MQ发送一条无效消息,以通知其他微服务当前具有包含权限的JWT令牌也无效。

–华伦丁
8月8日9:28

#19 楼

下面的方法可以提供两全其美的解决方案:
让“立即”表示“〜1分钟”。
情况:


用户尝试成功登录:
A.在令牌中添加一个“发出时间”字段,并根据需要保留到期时间。
B.存储用户密码的哈希值
或在用户表中创建一个新字段,例如tokenhash。
将令牌哈希值存储在生成的令牌中。


用户访问网址:
A.如果“发出时间”在“立即”范围内,请正常处理令牌。不要更改“发出时间”。根据“立即”的持续时间,这是一个易受攻击的持续时间。但是,像一两分钟这样的短持续时间不应太危险。 (这是性能和安全性之间的平衡)。三无须打这里的数据库。
B.如果令牌不在“立即”范围内,请对照数据库检查令牌哈希。如果还可以,请更新“发出时间”字段。如果还不行,则不要处理该请求(安全性最终得到加强)。


用户更改令牌哈希以保护帐户。在“即时”将来,该帐户将受到保护。


,我们将数据库查找保存在“即时”范围内。在“立即”持续时间内从客户端发送。

评论


这对于仅使用jwts而不进行数据库查找很有用,但是如何执行注销呢?从数据库中删除令牌哈希吗?

–funseiki
7月27日12:53

@funseiki是的。在用户注销时,可以从客户端删除令牌,并将令牌哈希更改为特殊值。在“即刻”的将来,所有其他可能的副本也将变得无效,因此强制重新登录。缺点:它会从所有设备中注销用户(尚未考虑如何克服这一问题)。

–codeman48
7月28日13:29

#20 楼

使用JWT的刷新...
我实用的一种方法是存储刷新令牌(可以是GUID)和对应的刷新令牌ID(无论完成多少刷新都不会更改) )添加到数据库中,并在生成用户的JWT时将其添加为对用户的声明。可以使用数据库的替代方式,例如。内存缓存。但是我在这个答案中使用数据库。
然后,创建一个JWT刷新Web API端点,客户端可以在JWT到期之前调用它。调用刷新时,请从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。

#21 楼

不使用刷新JWT ...
会想到两种攻击情形。一种是关于受损的登录凭据。另一种是JWT的实际盗窃。
对于受损的登录凭据,当发生新登录时,通常会向用户发送电子邮件通知。因此,如果客户不同意登录,则建议他们重置凭据,这应该将密码的上次设置时间保存到数据库/缓存中(在下次登录时也要设置此时间)用户在初始注册时设置密码)。只要授权了用户操作,就应该从数据库/缓存中获取用户更改密码的日期时间,并将其与生成给定JWT的日期时间进行比较,并禁止在该日期之前生成的JWT的操作。 -凭证重置时间,因此实质上使这种JWT变得无用。这意味着将JWT的生成日期时间保存为JWT本身的声明。在ASP.NET Core中,可以使用策略/要求进行此比较,并且在失败时,将禁止客户端。因此,每当凭证重置完成时,这都会在后端注销全局用户。但是,在JWT到期之前可以采取什么措施阻止攻击者呢?这是实际的全局注销。它类似于上面针对凭证重置所述的内容。为此,通常将用户发起全局注销的日期时间保存在数据库/缓存中,并在授权用户操作时将其获取,并将其与给定JWT的生成日期时间进行比较,并禁止该操作用于在上述全局注销日期时间之前生成的JWT,因此实际上使此类JWT变得无用。可以使用ASP.NET Core中的策略/要求来完成此操作,如前所述。
现在,您如何检测JWT的盗窃案?我现在的答案是偶尔提醒用户全局注销并再次登录,因为这肯定会使攻击者注销。

#22 楼

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


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

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

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


JWT有效吗? br />
让我们看一个实际的情况。

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

,晚上9点,他丢失了设备B。他立即从设备A注销。这意味着现在数据库X用户条目的LastValidTime为“ ThatDate: 9:00:xx:xxx”,并以“假”身份登录。输入是错误的,所以我们不允许。

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

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

#23 楼

IAM解决方案,例如Keycloak(我已经尝试过),提供了令牌撤销端点,例如
/realms/{realm-name}/protocol/openid-connect/revoke

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

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

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

#24 楼

另一种选择是只为关键的API端点提供一个中间件脚本。
如果管理员使令牌无效,则该中间件脚本将在数据库中检入。必须立即完全阻止用户访问。

#25 楼

在此示例中,我假设最终用户也有一个帐户。如果不是这种情况,则其余方法不太可能起作用。
创建JWT时,将其持久保存在与登录帐户相关联的数据库中。这确实意味着JWT,您可以提取有关用户的其他信息,因此根据环境的不同,可能不行。
在每次请求之后,您不仅要执行(我希望)随附的标准验证,无论您使用哪种框架(用于验证JWT均有效),它还包括诸如用户ID或其他令牌(需要与数据库中的令牌匹配)之类的东西。
注销时,删除cookie(如果使用),并使数据库中的JWT(字符串)无效。如果无法从客户端删除cookie,那么至少注销过程将确保令牌被销毁。
我发现这种方法与另一个唯一标识符一起使用(因此,数据库,并且可用于前端),因此会话非常灵活

#26 楼

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

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

替代黑名单,使用相同的解决方案,您可以使用每个会话的redis中保存的哈希来完成此操作,正如其他答案所指出的那样(不确定如果许多用户登录,这样做会更有效虽然如此)。

听起来很复杂吗?

免责声明:我尚未使用Redis。

#27 楼

如果您使用的是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);
    });
}


#28 楼

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

评论


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

–user2555515
19年4月9日在17:45

@ user2555515如果对存储在数据库中的令牌进行了加密(就像应该对存储在数据库中的任何密码进行加密)一样,此解决方案也可以正常工作。无状态JWT和有状态JWT之间存在差异(这与会话非常相似)。有状态的JWT可以从维护令牌白名单中受益。

–重块
19年7月28日在4:55