对于我正在从事的一个新的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 使用指定的令牌创建数据库。

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


当前回答

下面是如何做到这一点,而不必对每个请求都调用数据库:

在内存缓存中保留一个有效标记的hashmap(例如一个大小有限的LRU) 当检查令牌时:如果令牌在缓存中,立即返回结果,不需要数据库查询(大多数情况下)。否则,执行全面检查(查询数据库,检查用户状态和无效令牌…)。然后更新缓存。 当令牌失效时:将其添加到数据库的黑名单中,然后更新缓存,如果需要,向所有服务器发送信号。

请记住,缓存的大小应该是有限的,就像LRU一样,否则可能会耗尽内存。

其他回答

我一直在考虑的一种方法是在JWT中始终有一个iat(发布时间)值。然后,当用户注销时,将时间戳存储在用户记录上。在验证JWT时,只需将iat与上次注销的时间戳进行比较。如果iat更老,那么它是无效的。是的,您必须访问DB,但如果JWT在其他方面有效,我将始终提取用户记录。

我所看到的主要缺点是,如果他们在多个浏览器中,或者有一个移动客户端,它会让他们退出所有的会话。

这也是一种很好的机制,可以使系统中的所有jwt无效。部分检查可以针对最后有效的iat时间的全局时间戳。

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中的声明中。 每次用户注销时增加版本。 在验证令牌时,将其版本声明与存储在用户记录中的版本进行比较,如果不相同则拒绝。

无论如何,在大多数情况下都需要db访问以获取用户记录,因此这不会给验证过程增加太多开销。与维护黑名单不同,在黑名单中,由于需要使用连接或单独调用、清除旧记录等,数据库负载非常重要。

I ended up with access-refresh tokens, where refresh tokens uuids stored in database and access tokens uuids stored in cache server as a whitelist of valid access tokens. For example, I have critical changes in user data, for example, his access rights, next thing I do - I remove his access token from cache server whitelist and by the next access to any resource of my api, auth service will be asked for token's validity, then, if it isn't present in cache server whitelist, I will reject user's access token and force him to reauthorize by refresh token. If I want to drop user's session or all of his sessions, I simply drop all his tokens from whitelist and remove refresh tokens from database, so he musts re-enter credentials to continue accessing resources.

我知道,我的身份验证不再是无状态的,但公平地说,我为什么还要无状态的身份验证呢?

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

我认为这不是最好的解决方案,但对我来说是可行的。