对于我正在从事的一个新的node.js项目,我正在考虑从基于cookie的会话方法(我的意思是,将id存储到用户浏览器中包含用户会话的键值存储中)切换到使用JSON Web Tokens (jwt)的基于令牌的会话方法(没有键值存储)。

这个项目是一个利用socket的游戏。IO——在一个会话(web和socket.io)中有多个通信通道的情况下,有一个基于令牌的会话会很有用。

如何使用jwt方法从服务器提供令牌/会话失效?

我还想了解使用这种范例应该注意哪些常见的(或不常见的)陷阱/攻击。例如,如果这种模式容易受到与基于会话存储/cookie的方法相同/不同类型的攻击。

所以,假设我有以下内容(改编自this和this):

会话存储登录:

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 使用指定的令牌创建数据库。

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


当前回答

这主要是一个很长的评论,支持并建立在@mattway的回答上

考虑到:

本页上提出的其他一些解决方案提倡对每个请求都访问数据存储。如果您使用主数据存储来验证每个身份验证请求,那么我认为使用JWT而不是其他已建立的令牌身份验证机制的理由就更少了。如果每次都访问数据存储,那么实际上JWT是有状态的,而不是无状态的。

(如果您的站点接收到大量未经授权的请求,那么JWT将在不访问数据存储的情况下拒绝它们,这很有帮助。可能还有其他类似的用例。)

考虑到:

真正的无状态JWT身份验证无法在典型的、真实的web应用程序中实现,因为无状态JWT无法为以下重要用例提供即时和安全的支持:

用户帐号被删除/屏蔽/挂起。 完成用户密码的修改。 用户角色或权限发生变更。 用户被admin注销。 JWT令牌中的任何其他应用程序关键数据都由站点管理员更改。

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

因此:

我认为来自@mat -way的答案,#2 TokenBlackList,将是向基于JWT的身份验证添加所需状态的最有效方法。

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

对于每个通过初始JWT身份验证的身份验证请求,仍然需要调用内存中的数据库,但不必将整个用户集的密钥存储在其中。(对于一个特定的网站来说,这可能是也可能不是什么大问题。)

其他回答

Kafka消息队列和本地黑名单

我想过使用像kafka这样的消息系统。让我解释一下:

例如,您可以有一个微服务(我们称之为userMgmtMs服务),它负责登录和注销,并生成JWT令牌。然后将这个令牌传递给客户端。

现在客户端可以使用这个令牌来调用不同的微服务(让我们称之为pricesMs),在pricesMs中,将没有数据库检查用户表,从这个表中触发了最初的令牌创建。该数据库只能存在于userMgmtMs中。此外,JWT令牌应该包括权限/角色,这样pricesm就不需要从DB中查找任何东西来允许spring安全性工作。

JwtRequestFilter可以提供一个UserDetails对象,该对象由JWT令牌中提供的数据创建(显然没有密码),而不是去到pricesMs中的DB。

那么,如何注销或使令牌失效呢?由于我们不想在每次请求priecesm时都调用userMgmtMs的数据库(这会引入很多不必要的依赖关系),因此解决方案可以使用这个令牌黑名单。

我建议使用kafka消息队列,而不是保持这个黑名单集中,并依赖于所有微服务的一个表。

userMgmtMs仍然负责注销,一旦注销完成,它就会把它放入自己的黑名单(一个微服务之间不共享的表)。此外,它还将带有此令牌内容的kafka事件发送到订阅了所有其他微服务的内部kafka服务。

一旦其他微服务接收到kafka事件,它们也会将其放入内部黑名单。

即使一些微服务在注销时关闭了,它们最终也会重新启动,并在稍后的状态下接收消息。

由于kafka是这样开发的,客户端有他们自己的引用,他们读了哪些消息,确保没有客户端,down或up将错过任何这些无效的令牌。

The only issue again what I can think of is that the kafka messaging service will again introduce a single point of failure. But it is kind of reversed because if we have one global table where all invalid JWT tokens are saved and this db or micro service is down nothing works. With the kafka approach + client side deletion of JWT tokens for a normal user logout a downtime of kafka would in most cases not even be noticeable. Since the black lists are distributed among all microservies as an internal copy.

在关闭的情况下,你需要使一个被黑客攻击的用户无效,kafka宕机了,这就是问题开始的地方。在这种情况下,作为最后的手段改变秘密可能会有所帮助。或者在这样做之前确保卡夫卡已经起床了。

免责声明:我还没有实现这个解决方案,但不知怎么的,我觉得大多数提议的解决方案都否定了JWT令牌的想法,因为它有一个中央数据库查找。所以我在考虑另一种解决方案。

请让我知道你的想法,它是有意义的还是有一个明显的原因为什么它不能?

每个用户字符串唯一,全局字符串散列在一起作为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位秘密)

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

使用刷新jwt…

我采用的一种比较实用的方法是在数据库中存储一个刷新令牌(可以是GUID)和对应的刷新令牌ID(无论进行多少次刷新都不会改变),并在生成用户的JWT时将它们作为用户的声明添加。可以使用数据库的替代方案,例如内存缓存。但我用的是数据库。

然后,创建一个JWT刷新Web API端点,客户机可以在JWT到期之前调用该端点。当调用刷新时,从JWT中的声明中获取刷新令牌。

在对JWT刷新端点的任何调用中,在数据库上验证当前刷新令牌和刷新令牌ID为一对。生成一个新的刷新令牌,并使用刷新令牌ID替换数据库上的旧刷新令牌。记住它们是可以从JWT中提取出来的声明

从当前JWT中提取用户的声明。开始生成一个新的JWT的过程。将旧的刷新令牌声明的值替换为新生成的刷新令牌,该刷新令牌也是新保存在数据库上的。完成所有这些后,生成新的JWT并将其发送给客户端。

因此,在使用了刷新令牌之后,无论是目标用户还是攻击者,在数据库上使用未与其刷新令牌ID配对的/刷新令牌的任何其他尝试都不会导致生成新的JWT,从而阻止任何拥有该刷新令牌ID的客户端不再能够使用后端,从而导致此类客户端(包括合法客户端)的完全注销。

这解释了基本信息。

The next thing to add to that is to have a window for when a JWT can be refreshed, such that anything outside that window would be a suspicious activity. For example, the window can be 10min before the expiration of a JWT. The date-time a JWT was generated can be saved as a claim in that JWT itself. And when such suspicious activity occurs, i.e. when someone else tries to reuse that refresh token ID outside or within the window after it has already been used within the window, should mark the refresh token ID as invalid. Hence, even the valid owner of the refresh token ID would have to log in afresh.

如果在数据库上找不到与所提供的刷新令牌ID配对的刷新令牌,则表明刷新令牌ID应该无效。因为空闲用户可能会尝试使用攻击者已经使用过的刷新令牌。

如前所述,在目标用户之前被攻击者窃取和使用的JWT,在用户尝试使用刷新令牌时也会被标记为无效。

唯一没有涉及的情况是,即使攻击者可能已经窃取了JWT,客户端也从未尝试刷新它。但是这种情况不太可能发生在不受攻击者监管(或类似)的客户端上,这意味着攻击者无法预测客户端何时停止使用后端。

如果客户端发起常规注销。应该通过注销从数据库中删除刷新令牌ID和相关记录,从而防止任何客户端生成刷新JWT。

我是这样做的:

生成一个唯一的散列,然后将其存储在redis和JWT中。这可以称为会话 我们还将存储特定JWT发出的请求数量——每次JWT被发送到服务器时,我们将请求增加整数。(这是可选的)

因此,当用户登录时,将创建一个唯一的散列,存储在redis中并注入到JWT中。

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

我们可以以此为基础,让我们的JWT更加安全,如下所示:

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

这意味着JWT是不断变化的,并防止过时的JWT被黑客攻击、窃取或其他东西。