对于我正在从事的一个新的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。

其他回答

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

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

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

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

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

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

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

应急计划

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

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

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

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

---------------- 这个答案一点迟到但可能会帮助别人 ----------------

从客户端,最简单的方法是从浏览器的存储中删除令牌。

但是,如果您想销毁节点服务器上的令牌-

JWT包的问题是它没有提供任何方法或方法来销毁令牌。 您可以使用上面提到的关于JWT的不同方法。但是这里我用的是jwt-redis。

所以为了在服务器端销毁令牌,你可以使用JWT -redis包而不是JWT

这个库(jwt-redis)完全重复了库jsonwebtoken的全部功能,只增加了一个重要的功能。Jwt-redis允许您将tokenIdentifier存储在redis中以验证有效性。redis中缺少tokenIdentifier使得令牌无效。要销毁jwt-redis中的令牌,有一个destroy方法

它是这样工作的:

从npm安装jwt-redis 创建:

Var redis = require('redis'); var JWTR = require('jwt-redis').default; var redisClient = redis.createClient(); var jwtr = new jwtr (redisClient); Const secret = 'secret'; const tokenIdentifier = 'test'; const payload = {jti: tokenIdentifier};//你也可以在payload中放入其他数据 jwtr。号(载荷、秘密) 不要犹豫((令牌)= > { //你的代码 }) .catch((错误)= > { //错误处理 });

验证:

jwtr。验证(令牌,秘密);

摧毁:

//如果jti在token的签名过程中传递,那么tokenIdentifier else token jwtr.destroy(tokenIdentifier或token)

注意:

1).你可以在token的登录过程中提供expiresIn,就像JWT中提供的一样。

2).如果在token的签名过程中没有传递jti,那么jti将由库随机生成。

也许这能帮到你或其他人。谢谢。

以下方法可以提供两全其美的解决方案:

让“立即”表示“~1分钟”。

例:

User attempts a successful login: A. Add an "issue time" field to the token, and keep the expiry time as needed. B. Store the hash of user's password's hash or create a new field say tokenhash in the user's table. Store the tokenhash in the generated token. User accesses a url: A. If the "issue time" is in the "immediate" range, process the token normally. Don't change the "issue time". Depending upon the duration of "immediate" this is the duration one is vulnerable in. But a short duration like a minute or two shouldn't be too risky. (This is a balance between performance and security). Three is no need to hit the db here. B. If the token is not in the "immediate" range, check the tokenhash against the db. If its okay, update the "issue time" field. If not okay then don't process the request (Security is finally enforced). User changes the tokenhash to secure the account. In the "immediate" future the account is secured.

我们将数据库查询保存在“immediate”范围内。 如果在“即时”持续时间内有来自客户端的大量请求,那么这是最有益的。

如果您希望能够撤销用户令牌,您可以跟踪DB上所有发出的令牌,并检查它们在类似会话的表上是否有效(存在)。 缺点是每次请求都要访问DB。

我还没有尝试过,但我建议使用以下方法来允许令牌撤销,同时将DB命中保持在最小值-

为了降低数据库检查率,根据某种确定性关联将所有已发行的JWT令牌分成X组(例如,按用户id的第一个数字分为10组)。

每个JWT令牌将保存组id和令牌创建时创建的时间戳。例如,{"group_id": 1, "timestamp": 1551861473716}

服务器将在内存中保存所有组id,每个组都有一个时间戳,该时间戳指示属于该组的用户的最后一次注销事件是什么时候。 例如,{"group1": 1551861473714, "group2": 1551861487293,…}

使用带有较旧组时间戳的JWT令牌的请求将被检查是否有效(DB hit),如果有效,将发出一个带有新时间戳的新JWT令牌供客户端将来使用。 如果令牌的组时间戳较新,则相信JWT (No DB hit)。

So -

We only validate a JWT token using the DB if the token has an old group timestamp, while future requests won't get validated until someone in the user's group will log-out. We use groups to limit the number of timestamp changes (say there's a user logging in and out like there's no tomorrow - will only affect limited number of users instead of everyone) We limit the number of groups to limit the amount of timestamps held in memory Invalidating a token is a breeze - just remove it from the session table and generate a new timestamp for the user's group.