使JSON Web令牌无效

fun*_*iki 357 javascript session session-cookies node.js jwt

对于我正在研究的新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});
    });
}
Run Code Online (Sandbox Code Playgroud)

基于令牌的登录:

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});
    });
}
Run Code Online (Sandbox Code Playgroud)

-

会话存储方法的注销(或无效)需要使用指定的令牌更新KeyValueStore数据库.

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

Mat*_*Way 315

我也一直在研究这个问题,虽然下面没有一个想法是完整的解决方案,但它们可能会帮助其他人排除想法或提供更多想法.

1)只需从客户端删除令牌

显然,这对服务器端安全性没有任何作用,但它确实通过删除令牌来阻止攻击者(即,他们必须在注销之前窃取令牌).

2)创建令牌黑名单

您可以将无效令牌存储到其初始到期日期,并将它们与传入请求进行比较.这似乎否定了首先完全基于令牌的原因,因为您需要为每个请求触摸数据库.存储大小可能会更低,因为您只需要存储在注销和到期时间之间的令牌(这是一种直觉,并且肯定取决于上下文).

3)只需保持令牌到期时间短并经常旋转它们

如果您以足够短的间隔保持令牌到期时间,并让正在运行的客户端跟踪并在必要时请求更新,则数字1将有效地用作完整的注销系统.这种方法的问题在于,它使得用户无法在关闭客户端代码之间保持登录(取决于您到达时间间隔的时间长度).

临时计划

如果发生紧急情况或用户令牌遭到入侵,您可以做的一件事是允许用户使用其登录凭据更改基础用户查找ID.这将使所有关联的令牌无效,因为将无法再找到关联的用户.

我还想注意,最后一个登录日期包含令牌是一个好主意,这样您就可以在一段遥远的时间后强制执行重新登录.

关于使用令牌的攻击的相似性/差异,本文解决了以下问题:http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/

  • 用户更改密码时使令牌无效的常用方法是使用密码哈希对令牌进行签名.因此,如果密码更改,则任何以前的令牌都会自动无法验证.您可以通过在用户记录中包含last-logout-time并使用last-logout-time和password hash的组合来对令牌进行签名,从而将其扩展为注销.这需要在每次需要验证令牌签名时进行数据库查找,但无论如何您可能正在查找用户. (174认同)
  • @TravisTerry你的方法如果在整体应用程序中有用,但为了在微服务应用程序中实现它,你需要所有服务来存储用户的密码和上次登录或发出请求来获取它们,这两者都是坏主意。 (12认同)
  • 这篇文章写得很好,是上面"2)"的详细版本.虽然它工作正常,但我个人认为传统会话商店并没有太大区别.我想存储要求会更低,但你仍然需要一个数据库.JWT对我来说最大的吸引力在于根本不使用数据库进行会话. (6认同)
  • 黑名单可以通过将其保留在内存中来提高效率,因此只需要记录数据库以记录失效并删除过期的失效,并且仅在服务器启动时读取.在负载平衡架构下,内存中黑名单可以短时间间隔(如10秒)轮询数据库,从而限制无效令牌的曝光.这些方法允许服务器在没有按请求数据库访问的情况下继续验证请求. (4认同)
  • 优秀的方法.我的直觉是做所有3的组合,和/或,在每个"n"个请求之后请求一个新的令牌(而不是一个计时器).我们使用redis进行内存中的对象存储,我们可以很容易地将它用于#2情况,然后延迟就会减少. (3认同)
  • 另一种方法是为每个用户使用随机秘密,并将该秘密与数据库中的用户一起保存。当用户注销或更改密码时,只需更改数据库中的机密即可。 (3认同)
  • 这篇 [编码恐怖帖子](http://blog.codinghorror.com/your-session-has-timed-out/) 提供了一些建议:保持会话承载 cookie(或令牌)简短但使其对用户不可见 - 这似乎符合#3。我自己的直觉(也许是因为它更传统)只是让令牌(或它的散列)作为进入白名单会话数据库的键(类似于#2) (2认同)

小智 76

上面发布的想法很好,但是让所有现有JWT无效的一种非常简单易行的方法就是改变秘密.

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

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

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

  • 我认为这种方法并不理想.虽然它可以工作并且当然很简单,但想象一下您使用公钥的情况 - 您不希望在任何时候想要使单个令牌无效时重新创建该密钥. (8认同)
  • 这是非常糟糕的解决方案.使用JWT的主要原因是它是无状态和规模.使用动态秘密引入了一个状态.如果服务跨多个节点进行群集,则每次发出新令牌时都必须同步秘密.您必须将秘密存储在数据库或其他外部服务中,这只是重新发明基于cookie的身份验证 (8认同)
  • @TuomasToivonen,但你必须签署一个秘密的JWT,并能够用相同的秘密验证JWT.因此,您必须将密钥存储在受保护的资源上.如果密钥被泄露,您必须更改密码并将更改分发给每个节点.具有群集/扩展功能的托管服务提供商通常允许您在其服务中存储机密,以便轻松可靠地分发这些机密. (4认同)

Ed *_*d J 59

这主要是一篇长篇评论,支持并建立在@mattway的答案之上

鉴于:

此页面上的其他一些建议的解决方案主张在每个请求上访问数据存储区.如果您点击主数据存储区以验证每个身份验证请求,那么我认为使用JWT而不是其他已建立的令牌身份验证机制的理由更少.如果你每次都去数据存储区,你基本上使JWT成为有状态,而不是无状态.

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

鉴于:

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

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

用户密码已更改.

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

用户已由admin注销.

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

在这些情况下,您不能等待令牌过期.令牌失效必须立即发生.此外,您不能相信客户端不会保留和使用旧令牌的副本,无论是否具有恶意.

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

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

您仍然必须为通过初始JWT身份验证的每个身份验证请求调用内存数据库,但您不必为其中的整个用户组存储密钥.(对于给定的站点,这可能是也可能不是什么大问题.)

  • 我不同意你的回答.点击数据库不会使任何状态; 在你的后端存储状态.未创建JWT,因此您不必在每个请求上访问数据库.使用JWT的每个主要应用程序都由数据库支持.JWT解决了一个完全不同的问题.https://en.wikipedia.org/wiki/Stateless_protocol (11认同)
  • @ zero01alpha身份验证:这是使用JWT的最常见方案.一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源.信息交换:JSON Web令牌是在各方之间安全传输信息的好方法.因为JWT可以签名,所以可以确定发件人是他们所说的人.请参阅https://jwt.io/introduction (7认同)
  • 哈!感谢一年前评论的快速回复 (6认同)
  • @Julian我不同意你的不同意见:) JWT解决问题(对于服务)需要访问为任何给定客户端提供授权信息的集中实体.因此,代替服务A而服务B必须访问某些资源以查明客户端X是否具有执行某些操作的权限,服务A和B从X接收证明其权限的令牌(最常见的是由第3个发布)派对).无论如何,JWT是一种有助于避免系统中服务之间共享状态的工具,尤其是当它们由多个服务提供商控制时. (5认同)
  • @Julian你能详细说明一点吗?JWT真正解决了哪个问题呢? (4认同)
  • 同样来自 https://jwt.io/introduction/ “如果 JWT 包含必要的数据,则可能会减少查询数据库以执行某些操作的需要,尽管情况可能并不总是如此。” (4认同)
  • 如果每次数据库都被命中,那么拥有刷新令牌的目的就失去了。 (2认同)

Daf*_*onk 38

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

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

任何时候你想要使旧jwts无效,只需碰撞用户jwt版本号.

  • 这是一个有趣的想法,唯一的存储版本,作为令牌的目的的一部分是它是无状态的,不需要使用数据库.硬编码版本会使其难以碰撞,数据库中的版本号会否定使用令牌的一些好处. (13认同)
  • 据推测,您已经在令牌中存储了用户ID,然后查询数据库以检查用户是否存在/有权访问api端点.因此,您不会通过将jwt令牌版本号与用户上的版本号进行比较来执行任何额外的数据库查询. (13认同)
  • 如果用户从多个设备登录怎么办?是否应该在其中使用一个令牌,还是应该使所有先前的令牌无效? (11认同)
  • 我同意@SergioCorrea这将使JWT几乎与任何其他令牌认证机制一样有状态. (10认同)
  • 我不应该说,因为在很多情况下你可能会使用带有完全没有触及数据库的验证的令牌.但我认为在这种情况下很难避免. (5认同)
  • 将令牌保留在数据库中而不是在版本中添加额外的复杂性会不会更简单?无论如何都会打到数据库.我宁愿根本不打数据库. (4认同)
  • @stewenson - 也许您已登录共享/公共设备,只希望从那里删除您的令牌. (3认同)

Ash*_*ian 31

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

目标:

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

解决方案:

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

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

缺点:

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

优点:

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

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

  • 或者您可以从sesion /本地存储或cookie中删除JWT. (4认同)
  • 谢谢@Ashtonian。经过广泛的研究后,我放弃了 JWT。除非您不遗余力地保护密钥,或者除非您委托安全的 OAuth 实现,否则 JWT 比常规会话更容易受到攻击。查看我的完整报告:http://by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html (3认同)
  • 使用刷新令牌是允许列入黑名单的关键。很好的解释:https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/ (2认同)
  • 在我看来,这似乎是最好的答案,因为它结合了短期访问令牌和可以列入黑名单的长期刷新令牌。注销时,客户端应删除访问令牌,以便第二个用户无法访问(即使访问令牌在注销后将在几分钟内保持有效)。@Joe Lapp 说黑客(第二个用户)即使在访问令牌被删除后也能获得它。如何? (2认同)

小智 13

我一直在考虑的方法是iat在JWT中始终具有(发布于)值.然后,当用户注销时,将该时间戳存储在用户记录中.验证JWT时,只需比较iat上次登出的时间戳即可.如果iat年龄较大,那么它无效.是的,您必须转到数据库,但如果JWT无效,我将始终提取用户记录.

我看到的主要缺点是,如果它们在多个浏览器中,或者也有移动客户端,它会将它们记录在所有会话中.

这也可以是使系统中所有JWT无效的良好机制.部分检查可能是针对上一个有效iat时间的全局时间戳.

  • 像所有其他提议的解决方案一样,这个解决方案需要数据库查找,这就是这个问题存在的原因,因为避免查找是这里最重要的事情!(性能、可扩展性)。在正常情况下,您不需要数据库查找来获得用户数据,您已经从客户端获得了它。 (4认同)

Ama*_*pta 11

------------------------这个答案有点晚了,但可能会对某人有所帮助------ -----------

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

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

JWT 包的问题在于它不提供任何方法或方式来销毁令牌。您可以对上面提到的 JWT 使用不同的方法。但在这里我使用 jwt-redis。

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

这个库 (jwt-redis) 完全重复了库 jsonwebtoken 的全部功能,并添加了一个重要的补充。Jwt-redis 允许您将令牌标签存储在 redis 中以验证有效性。redis 中缺少令牌标签使令牌无效。要销毁jwt-redis中的token,有一个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
    });
Run Code Online (Sandbox Code Playgroud)

3)验证——

jwtr.verify(token, secret);
Run Code Online (Sandbox Code Playgroud)

4)摧毁-

jwtr.destroy(token)
Run Code Online (Sandbox Code Playgroud)

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

可能这对某人有帮助

  • 谢谢@Eren,是的,如果您有多个服务器并且令牌的配置有任何更改,那么您当然需要再次同步它。还有一种方法可以通过调用 jwtr.destroy(jti) (“jti 是 json 令牌标识符”)来通过标签销毁令牌,您可以在签署令牌时将其传递到有效负载中,如果未传递,则库将随机生成令牌。请参阅此处了解更多详细信息“https://www.npmjs.com/package/jwt-redis” (2认同)

小智 8

我在这里有点晚了,但我想我有一个不错的解决方案.

我的数据库中有一个"last_password_change"列,用于存储上次更改密码的日期和时间.我还将发行日期/时间存储在JWT中.在验证令牌时,我会检查在发出令牌后密码是否已更改,以及令牌是否被拒绝,即使它尚未过期.

  • 需要db查找! (11认同)

Mar*_*sel 6

每个用户唯一的字符串,以及散列在一起的全局字符串

作为 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);
Run Code Online (Sandbox Code Playgroud)

例如用法参见https://jwt.io(不确定他们处理动态 256 位机密)

  • @giantas,我认为马克的意思是签名部分。因此,不要只使用单个密钥来签署 JWT,而是将其组合为每个客户端唯一的密钥。因此,如果您想让某个用户的所有会话无效,只需更改该用户的密钥即可;如果您想让系统中的所有会话无效,只需更改该全局单个密钥即可。 (3认同)
  • 更多细节就足够了 (2认同)
  • 很好,但显然,仍然需要数据库查找 (2认同)

Jam*_*111 6

我按照以下方式做到了:

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

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

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

我们可以以此为基础进行扩展,使我们的JWT更加安全,具体方法如下:

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

这意味着JWT会不断变化,并阻止陈旧的JWT被黑客攻击、被盗或发生其他情况。


Edu*_*rdo 6

保持这样的内存列表

user_id   revoke_tokens_issued_before
-------------------------------------
123       2018-07-02T15:55:33
567       2018-07-01T12:34:21
Run Code Online (Sandbox Code Playgroud)

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

  • 大多数生产站点在不止一台服务器上运行,因此此解决方案将不起作用。添加 Redis 或类似的 interpocess 缓存会使系统显着复杂化,并且往往带来的问题多于解决方案。 (3认同)

dav*_*mer 5

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


小智 5

您可以在用户的​​文档/记录上的数据库中包含"last_key_used"字段.

当用户使用user登录并传递时,生成新的随机字符串,将其存储在last_key_used字段中,并在签名时将其添加到有效负载中.

当用户使用令牌登录时,请检查DB中的last_key_used以匹配令牌中的last_key_used.

然后,当用户执行注销时,或者如果要使令牌无效时,只需将"last_key_used"字段更改为另一个随机值,任何后续检查都将失败,从而强制用户使用用户登录并再次传递.


Sha*_*eer 5

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

清除客户端存储/会话

分别在登录或注销时更新用户表上次登录日期时间和注销日期时间。所以登录日期时间总是应该大于注销(或者如果当前状态是登录并且尚未注销,则保持注销日期为空)

这比保留额外的黑名单表和定期清除要简单得多。多设备支持需要额外的表格来保持登录、注销日期以及一些额外的细节,比如操作系统或客户端的细节。