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

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


当前回答

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

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

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

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

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

其他回答

即使从存储中删除令牌,它仍然有效,但只是在短时间内有效,以降低它被恶意使用的可能性。

您可以创建一个拒绝列表,一旦从存储中删除令牌,就可以将令牌添加到该列表中。如果你有一个微服务,所有其他使用这个令牌的服务都必须添加额外的逻辑来检查这个列表。这将集中您的身份验证,因为每个服务器都必须检查一个集中的数据结构。

在经过一些研究后,我的观点如下。 在登出期间,确保发生了以下事情…

清除客户端存储/会话

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

这远比保持额外的黑名单表和定期清洗简单。多设备支持需要额外的表来保存loggedIn、注销日期和一些额外的详细信息,如操作系统或客户端详细信息。

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.

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

Haven't tried this yet, and it is uses a lot of information based on some of the other answers. The complexity here is to avoid a server side data store call per request for user information. Most of the other solutions require a db lookup per request to a user session store. That is fine in certain scenarios but this was created in an attempt to avoid such calls and make whatever required server side state to be very small. You will end up recreating a server side session, however small to provide all the force invalidation features. But if you want to do it here is the gist:

目标:

减少数据存储的使用(无状态)。 能够强制注销所有用户。 能力强制注销任何个人在任何时间。 在一段时间后要求密码重新输入的能力。 能够与多个客户一起工作。 当用户从特定客户端单击注销时,强制重新登录的能力。(为了防止有人在用户离开后“取消删除”客户端令牌-查看评论了解更多信息)

解决方案:

Use short lived (<5m) access tokens paired with a longer lived (few hours) client stored refresh-token. Every request checks either the auth or refresh token expiration date for validity. When the access token expires, the client uses the refresh token to refresh the access token. During the refresh token check, the server checks a small blacklist of user ids - if found reject the refresh request. When a client doesn't have a valid(not expired) refresh or auth token the user must log back in, as all other requests will be rejected. On login request, check user data store for ban. On logout - add that user to the session blacklist so they have to log back in. You would have to store additional information to not log them out of all devices in a multi device environment but it could be done by adding a device field to the user blacklist. To force re-entry after x amount of time - maintain last login date in the auth token, and check it per request. To force log out all users - reset token hash key.

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

缺点:

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

优点:

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

With this solution an in memory data store like reddis isn't needed, at least not for user information as you are as the server is only making a db call every 15 or so minutes. If using reddis, storing a valid/invalid session list in there would be a very fast and simpler solution. No need for a refresh token. Each auth token would have a session id and device id, they could be stored in a reddis table on creation and invalidated when appropriate. Then they would be checked on every request and rejected when invalid.

我要回答的是,当我们使用JWT时,我们是否需要提供从所有设备注销功能。这种方法将对每个请求使用数据库查找。因为即使服务器崩溃,我们也需要持久安全状态。在用户表中,我们将有两列

LastValidTime(默认为创建时间) 登录(默认值:true)

每当有来自用户的注销请求时,我们将LastValidTime更新为当前时间,login - in更新为false。如果有一个登录请求,我们不会改变LastValidTime,但登录将被设置为true。

当我们创建JWT时,我们将在有效载荷中有JWT创建时间。当我们授权一项服务时,我们将检查3个条件

JWT有效吗 JWT有效负载创建时间是否大于用户LastValidTime 用户是否已登录

让我们来看一个实际的场景。

用户X有两个设备A和b,他在晚上7点使用设备A和设备b登录到我们的服务器(假设JWT过期时间是12小时)。A和B都有JWT, createdTime: 7pm

在晚上9点,他丢失了他的设备b,他立即从设备a注销。这意味着现在我们的数据库X用户条目的LastValidTime为“ThatDate:9:00:xx:xxx”,登录为“false”。

在9:30,Mr.Thief尝试使用设备b登录,我们将检查数据库,即使登录是假的,所以我们不允许。

晚上10点,x先生从他的设备A登录,现在设备A有JWT,并创建了时间:10点。现在database Logged-In被设置为true

晚上10点半,小偷先生试图登录。即使“登录”是真的。数据库中的LastValidTime是晚上9点,但是B的JWT创建的时间是晚上7点。所以他不会被允许访问该服务。所以使用设备B没有密码,他不能使用已经创建的JWT在一个设备注销后。