据我了解,JWT 刷新令牌的典型(和简化)用法如下:
因此,当用户不小心泄露密码或丢失设备时,他可以简单地更改密码,这将导致所有先前发布的刷新令牌失效。
然而,这里明显的安全漏洞是短期访问令牌在接下来的 10 分钟内仍然完全有效。而 10 分钟,无论时间多短,对于恶意用户来说仍然是足够的时间造成一些损害。
我能想到的唯一可能的解决方案是:
维护访问令牌的黑名单或白名单。这使得刷新令牌的使用看起来非常多余。如果我们要在每次请求时访问数据库或保留一个列入黑名单的访问令牌的缓存列表,那么拥有刷新令牌的意义何在?
缩短访问令牌的有效期(例如:每 1 分钟而不是每 10 分钟)。这并不能解决问题本身,它只是做了一些损害控制,因为它缩短了恶意用户必须造成损害的时间窗口。每分钟访问数据库以获取新的访问令牌似乎并不比每次请求访问数据库好多少。
我一直在研究完全相同的问题。虽然我不能说我是这个主题的任何权威,但我很高兴分享我基于大量研究和构建概念证明得出的结论。
要求是具有即时令牌访问令牌撤销功能。在应用程序的正常运行过程中,恶意用户访问某人帐户的情况的实际概率相对较低。这并不是说它不应该被考虑在内,但是对于进入系统的 99.9% 的请求来说,情况并非如此,因此让它在所有不存在的请求上对照数据库检查访问令牌在我看来,物质是糟糕的设计。
然而,无论设计是否糟糕,它都不会改变需求。要求每分钟刷新访问令牌似乎并没有好多少,因为这会给身份验证服务器和数据库带来巨大的压力。管理内存中的访问令牌撤销列表不会有太大作用,因为它不会在实例之间共享。根据您的用户量,这可能能够暂时使用数据库,但我认为它不会扩展到超过某个点。
我选择采用的解决方案是使用共享内存数据库/缓存。我评估了 Cassandra、Redis 和 Apache Ignite,暂时决定使用 Ignite。因为我不确定一旦投入生产,它将如何执行,因此我将这些组件轻松地替换为另一个内存解决方案,以防性能不足。
我有一个 JWT 过滤器,负责验证每个请求,最后我调用共享缓存来检查访问令牌撤销列表。我预计该列表在绝大多数情况下都是空的。为了进一步减少潜在的性能下降,我在撤销令牌之前使用 MD5 将令牌哈希为大约 40 个字符。此功能使我能够拥有 1 小时长的访问令牌生命周期和 18 小时生命周期的刷新令牌,而不必担心在需要时无法删除恶意用户。
就我个人而言,我看不出有什么方法可以在后端跟踪某些用户状态。诀窍在于,您仍然可以轻松添加后端实例来扩展应用程序。