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

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


当前回答

代币的有效期为1天 每天建立一个黑名单。 将无效/注销令牌放入黑名单

对于令牌验证,首先检查令牌的过期时间,如果令牌没有过期,则检查黑名单。

对于长会话需求,应该有一种机制来延长令牌过期时间。

其他回答

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.

简单地创建添加以下对象到你的用户模式:

const userSchema = new mongoose.Schema({
{
... your schema code,
destroyAnyJWTbefore: Date
}

当你在/login上收到POST请求时,将这个文档的日期更改为date .now()

最后,在您的身份验证检查代码中,即在您的中间件中检查isauthenticated或protected或任何您使用的名称,只需添加一个检查myjwt的验证。iat大于userDoc.destroyAnyJWTbefore。

如果您想在服务器端销毁JWT,那么在安全性方面,这个解决方案是最好的。 这个解决方案不再依赖于客户端,它打破了使用jwt的主要目标,即停止在服务器端存储令牌。 这取决于您的项目上下文,但最可能的是您想要从服务器上销毁JWT。

如果您只想从客户端销毁令牌,只需从浏览器中删除cookie(如果您的客户端是浏览器),在智能手机或任何其他客户端上也可以这样做。

如果选择从服务器端销毁令牌,我建议您使用Radis通过实现其他用户提到的黑名单样式来快速执行此操作。

现在的主要问题是:jwt无用吗?上帝知道。

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

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

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

我也一直在研究这个问题,虽然下面的想法都不是完整的解决方案,但它们可能会帮助其他人排除这些想法,或者提供进一步的解决方案。

1)简单地从客户端删除令牌

显然,这对服务器端安全没有任何帮助,但它确实通过删除令牌来阻止攻击者。他们必须在登出之前窃取令牌)。

2)创建一个令牌区块列表

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

3)保持代币到期时间短,并经常轮换

如果您将令牌过期时间保持在足够短的间隔内,并让运行中的客户端保持跟踪并在必要时请求更新,那么第1条将有效地作为一个完整的注销系统。这种方法的问题是,它不可能在客户端代码关闭之间保持用户登录(取决于您设置的过期间隔多长时间)。

应急计划

如果出现紧急情况,或者用户令牌被破坏,您可以做的一件事是允许用户使用其登录凭据更改底层用户查找ID。这将使所有关联的令牌无效,因为将不再能够找到关联的用户。

我还想指出,在令牌中包含最后一次登录日期是个好主意,这样您就可以在一段时间后强制重新登录。

关于使用令牌的攻击的相似/不同之处,这篇文章解决了这个问题:https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies-vs-token.markdown

我是这样做的:

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

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

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

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

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

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