在Node.js中更改密码和注销时使JWT失效的最佳实践是什么?

84
我想了解在更改密码/注销时无需访问数据库来使JWT无效的最佳实践。 我有以下思路来处理上述2种情况,需要访问用户数据库:
1. 在更改密码时,我检查存储在用户数据库中的密码(已经哈希)。 2. 在注销时,我在用户数据库中保存最后一次注销的时间,因此通过比较令牌创建时间和注销时间,我可以使其无效。
但这2个情况的代价是每当用户调用API时都要访问用户数据库。欢迎分享任何最佳实践。
更新: 我不认为我们可以在没有访问数据库的情况下使JWT无效。所以我想到了一个解决方案,如果您有任何疑虑,请查看我的答案。

1
你不能这样做。如果需要撤销,不要使用JWT。通常情况下,不要将JWT用作会话的替代品。这不是它们的预期目的,也不适合作为会话的替代品。请参阅https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens。 - user229044
5个回答

95

当未使用刷新令牌时:

1.在更改密码时: 当用户更改密码时,在用户数据库中记录更改密码的时间,因此当更改密码的时间大于令牌创建时间时,令牌就无效了。因此,剩余会话将很快注销。

2.当用户退出登录时: 当用户退出登录时,将令牌保存在单独的数据库中(例如:InvalidTokenDB),并在令牌过期时从数据库中删除令牌。因此,用户从相应设备注销登录后,他在其他设备上的会话不受影响。

因此,在使JWT失效时,我按照以下步骤操作:

  1. 检查令牌是否有效。
  2. 如果有效,则检查其是否存在于InvalidTokenDB中(一个存储已注销令牌的数据库,直到它们的过期时间)。
  3. 如果不存在,则检查用户数据库中的令牌创建时间和更改密码时间。
  4. 如果更改密码时间 < 令牌创建时间,则令牌有效。

上述方法的问题:

  1. 对于每个API请求,我需要遵循以上所有步骤,这可能会影响性能。

当使用刷新令牌时: 访问令牌有效期为1天,刷新令牌有效期为终身

1.在更改密码时: 当用户更改密码时,更改用户的刷新令牌。因此,剩余会话将很快注销。

2.当用户退出登录时: 当用户退出登录时,将令牌保存在单独的数据库中(例如:InvalidTokenDB),并在令牌过期时从数据库中删除令牌。因此,用户从相应设备注销登录后,他在其他设备上的会话不受影响。

因此,在使JWT失效时,我按照以下步骤操作:

  1. 检查令牌是否有效。
  2. 如果有效,则检查其是否存在于InvalidTokenDB中。
  3. 如果不存在,则检查用户数据库中的刷新令牌。
  4. 如果相等,则为有效令牌。
上述方法的问题:
  1. 对于每个api请求,我需要遵循以上所有步骤,这可能会影响性能。
  2. 如何使刷新令牌失效,因为刷新令牌没有有效期,如果被黑客使用,则身份验证仍然有效,请求将始终成功。
注意:尽管Hanz在“在基于令牌的身份验证中使用刷新令牌是否安全?”中建议了一种保护刷新令牌的方法,但我无法理解他的意思。任何帮助都会受到赞赏。

因此,如果有人有好的建议,欢迎您的评论。

更新: 我添加答案,以防止您的应用程序需要具有寿命到期的刷新令牌。此答案由Sudhanshu(https://stackoverflow.com/users/4062630/sudhanshu-gaur)提供。感谢Sudhanshu。所以我认为这是最好的方法, 不需要刷新令牌并且访问令牌无到期时间时:

当用户登录时,在他的用户数据库中创建一个无到期时间的登录令牌。

因此,在使JWT失效时,请按照以下步骤进行:

  1. 检索用户信息并检查令牌是否在其用户数据库中。如果是,则允许。
  2. 当用户注销时,仅从其用户数据库中删除此令牌。
  3. 当用户更改密码时,从其用户数据库中删除所有令牌,并要求他重新登录。

因此,使用这种方法,您不需要在数据库中存储注销令牌,直到它们过期,也不需要在更改密码时存储令牌创建时间,而这在上述情况下是必需的。但是,我认为只有在您的应用程序要求不需要刷新令牌且访问令牌没有到期时间的情况下,此方法才有效。

如果有人对此方法有疑虑,请告诉我。欢迎您的评论:)


我想到了和你一样的方法,但是你应该在更改密码字段上添加过期时间,请看下面我的答案 :) - Sudhanshu Gaur
1
而且,你可以使用Redis作为内存缓存,而不是普通的数据库,因此查找时间会非常短。 - Sudhanshu Gaur
@amiawizard,能告诉我你所指的场景是什么吗?我相信我已经回答了这个问题,“当用户修改密码时,在用户数据库中记录更改密码时间,因此当更改密码时间大于令牌创建时间时,令牌无效。因此剩余的会话将很快被注销。” - Gopinath Shiva
75
在数据库/数据存储中查找不会否定JWT的作用吗? - Metalstorm
1
如果您正在使用关系数据库管理系统(RDBMS),则可以添加一个列到用户表中,例如“access_token”,当用户注销时,只需清除该令牌。当用户登录时,更新该列以生成新的令牌。 - Konrad
显示剩余4条评论

19

目前我所知道的方法中,没有一种可以在不涉及数据库的情况下任意使令牌失效的方法。

如果您的服务可以在多个设备上访问,请小心使用第二种方法。考虑以下场景...

  • 用户使用iPad登录,Token 1被颁发并存储。
  • 用户在网站上登录。Token 2被颁发。用户退出登录。
  • 用户尝试使用iPad,Token 1是在用户从网站退出登录之前颁发的,现在被视为无效。

您可能需要查看“刷新令牌”的概念,尽管这些也需要数据库存储。

此外,还可以参见此处,其中进行了一次良好的SO讨论,讨论了类似的问题,特别是IanB的解决方案可以节省一些db调用。

建议的解决方案 就我个人而言,我会这样做...用户进行身份验证后,颁发一个带有短期到期时间(例如15分钟)和长期有效或无限期的刷新令牌。在数据库中存储此刷新令牌的记录。

每当用户“活动”时,每次颁发一个新的身份验证令牌(每次有效期为15分钟)。如果用户超过15分钟没有活动,然后进行请求(因此使用已过期的JWT),请检查刷新令牌的有效性。如果它有效(包括db检查),则颁发新的身份验证令牌。

如果用户在设备或网站上“注销”,则在客户端销毁访问刷新令牌,并重要地撤销所使用的刷新令牌的有效性。如果用户在任何设备上更改其密码,则撤销其所有刷新令牌,强制他们在访问令牌过期后再次登录。这确实留下了一个“不确定窗口”,但是在不每次命中db的情况下无法避免。

使用此方法还可以打开用户有可能“吊销”特定设备访问权限的可能性,就像许多主要Web应用程序所看到的那样。


当用户注销时,我如何在客户端上销毁所有现有的令牌?如果我这样做,那么它将在所有设备上注销。但是,这些令牌仍然处于有效状态。如果黑客仍然使用该令牌,则身份验证仍将是有效的(假设令牌有效期为1周)。这不是我想要的。我只想在各自的设备上注销用户,但令牌也应该得到保护。 - Gopinath Shiva
我已经在下面发布了我的解决方案,更新了问题,并且我也对我的建议答案有相应的担忧。欢迎您的评论。 - Gopinath Shiva
@gopinathshiva 当我在注销时说“删除现有令牌”,我的意思是删除该特定设备上存储的访问/刷新令牌的任何记录 - 是的,如果某些人可以访问,访问令牌仍将有效。在我的解决方案中,我建议使用短寿命的访问令牌来限制这个“机会窗口”(它总是存在的)。我建议你提供的解决方案实际上正在失去jwt的重点。 - DevFox
我认为你提供的解决方案实际上失去了JWT的全部意义。我不太明白你的意思,请解释一下你所说的解决方案是什么。我不想使用短暂的访问令牌,这就是为什么我建议这个解决方案的原因。 - Gopinath Shiva
不是有意冒犯,但我认为我没有理解错误——刷新令牌可以与用户或用户/设备组合相关联。例如,Auth0 :“刷新令牌可以针对每个应用程序、用户和设备组合进行发放和撤销。” - DevFox
显示剩余8条评论

11

我不确定我是否漏掉了什么,但我发现被接受的答案比必要的复杂。

我发现每次API请求都需要命中数据库来验证或使token失效,然而总体过程可以更简单。

每当创建JWT时,即在登录或更改/重置密码期间,将具有用户ID的JWT插入到表中,并为每个JWT维护一个jti(基本上是UUID编号)。同样的jti也进入JWT负载。有效地,jti唯一标识JWT。当从多个设备或浏览器访问帐户时,用户可能同时拥有多个JWT,在这种情况下,jti区分设备或用户代理。

因此,表模式将是,jti | userId。(当然还有主键)

对于每个API,检查jti是否在表中,这意味着JWT是有效的。

当用户更改或重置密码时,从数据库中删除该userId的所有jti。创建并插入带有新jti的新JWT到表中。这将使除更改或重置密码的设备之外的所有其他设备和浏览器的所有会话无效。

当用户退出时,删除该用户的特定jti,但不是所有jti。会有单点登录但没有单点注销。因此,当用户注销时,他不应从所有设备注销。但是,删除所有jtis也将从所有设备注销。

因此,这将是一个表,没有日期比较。如果使用刷新令牌或不使用刷新令牌,则情况相同。

然而,为了最小化数据库干扰和可能的延迟,缓存使用肯定会有所帮助,以缓解处理时间问题。

注意:如果您向下投票,请说明原因。


我不想每次使用jwt时都要检查数据库。在你的情况下,我必须这样做。我认为检查令牌是否无效要便宜得多,因为这不是常见情况。而且你甚至可以通过延迟(比如5分钟)使令牌无效,而不是有效性:它必须尽快有效。 - sigi
@sigi,我不明白您如何决定何时使用户在所有设备上的JWT无效。我想重新发行一个JWT,并在创建后的3秒钟内使其无效,但我不知道如何确定要使哪个JWT无效。 - Hanu
1
创建JWT时,将其存储在数据库中(因为仅在登录时发生,所以这是可以的)。 JWT随后具有过期日期,每次都会进行检查。除此之外,您还要检查它是否在黑名单上(可以是数据库表或红色)。当用户更改密码时,您会查找此用户的所有JWT,并检查所有仍然有效的JWT并将它们放入黑名单中。优点:此黑名单要小得多,可以轻松地保存在内存中。此外,黑名单落后几分钟也没关系。 - sigi
2
如果每次 API 调用都需要检查数据库,那么 JWT 的整个意义似乎就变得多余了。还不如使用会话。 - DollarAkshay

2
如果用户正在更改他们的密码,您将会访问数据库。但是不想为授权访问数据库?
我发现存储每个用户字符串和全局共享字符串的哈希值能够使我们在JWT实现中具有最大的灵活性。在这种情况下,我将存储密码的哈希值,并将其与全局字符串哈希在一起作为JWT密钥。

-3

我完全同意@gopinath的答案,只想补充一点,当所有令牌过期时,您还应该删除更改密码时间,例如假设您设置了每个令牌的3天到期时间,现在除了正常保存更改密码时间之外,您还可以将其到期时间设置为3天,因为显然在此之前的令牌将过期,因此无需再次检查每个令牌的到期时间是否大于更改密码时间。


1
很感激你的回答。我有一个问题,如果我说错了请原谅。如果您没有在数据库中存储更改密码的时间,那么登录令牌也会使用旧密码创建,对吗?例如,您已经使用手机登录,现在在电脑上更改了密码,但是手机上的会话仍然运行了3天。我认为,在这种情况下,会话不应该在手机上工作。正因为有这种情况,我才添加了存储更改密码时间的逻辑到数据库中。 - Gopinath Shiva
1
我收到了你的回答,但是我告诉你的问题不同。你提到模块会处理过期的令牌,我同意它应该这样做。但是这里的情况是这样的,比如说我在1月13日使用我的MOBILE(旧密码)登录应用程序,现在我在1月14日在PC上更改了应用程序密码。所以现在所有使用我的旧密码生成的先前令牌都不应该起作用。 - Gopinath Shiva
1
现在,如果我没有在数据库中存储更改密码的时间,那么我将无法注销使用旧密码生成的令牌。举个例子,在上面的例子中,生成的令牌在1月13日仍将在接下来的3天内有效(即如果令牌过期时间设置为3天,则直到1月16日)。你现在懂了吗? - Gopinath Shiva
1
嗨,Sudhanshu,我已经将你的评论作为我的更新答案添加了进去。请检查并进行编辑,如果需要的话 :) - Gopinath Shiva
1
很酷。肯定会帮助到别人 :) - Gopinath Shiva
显示剩余16条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接