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

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


当前回答

如果“注销所有设备”选项是可接受的(在大多数情况下是):

将令牌版本字段添加到用户记录中。 将此字段中的值添加到存储在JWT中的声明中。 每次用户注销时增加版本。 在验证令牌时,将其版本声明与存储在用户记录中的版本进行比较,如果不相同则拒绝。

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

其他回答

如果不对每个令牌验证进行DB查找,这似乎很难解决。我能想到的替代方案是在服务器端保留无效令牌的黑名单;当发生更改时,应该在数据库上进行更新,通过使服务器在重新启动时检查数据库以加载当前黑名单来持久化更改。

但是如果你把它放在服务器内存中(一个全局变量),那么如果你使用多个服务器,它就不能跨多个服务器伸缩,所以在这种情况下,你可以把它保存在一个共享的Redis缓存中,应该设置在某个地方持久化数据(数据库?文件系统?),以防它必须重新启动,每次新服务器启动时,它都必须订阅Redis缓存。

作为黑名单的替代方案,使用相同的解决方案,你可以像这个其他答案指出的那样,在每个会话中保存一个散列(虽然不确定在许多用户登录时是否更有效)。

听起来是不是很复杂?对我来说是这样的!

免责声明:我没有使用过Redis。

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

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

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

上面的想法很好,但是有一种非常简单的方法可以使所有现有的jwt无效,那就是简单地改变秘密。

如果您的服务器创建了JWT,用一个秘密(JWS)对其进行签名,然后将其发送给客户端,简单地更改秘密将使所有现有的令牌无效,并要求所有用户获得一个新的令牌进行身份验证,因为他们的旧令牌突然根据服务器变得无效。

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

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

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

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

任何时候你想要让旧的jwt失效,只要改变用户的jwt版本号。

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.

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