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

其他回答

这主要是一个很长的评论,支持并建立在@mattway的回答上

考虑到:

本页上提出的其他一些解决方案提倡对每个请求都访问数据存储。如果您使用主数据存储来验证每个身份验证请求,那么我认为使用JWT而不是其他已建立的令牌身份验证机制的理由就更少了。如果每次都访问数据存储,那么实际上JWT是有状态的,而不是无状态的。

(如果您的站点接收到大量未经授权的请求,那么JWT将在不访问数据存储的情况下拒绝它们,这很有帮助。可能还有其他类似的用例。)

考虑到:

真正的无状态JWT身份验证无法在典型的、真实的web应用程序中实现,因为无状态JWT无法为以下重要用例提供即时和安全的支持:

用户帐号被删除/屏蔽/挂起。 完成用户密码的修改。 用户角色或权限发生变更。 用户被admin注销。 JWT令牌中的任何其他应用程序关键数据都由站点管理员更改。

在这些情况下,您不能等待令牌到期。令牌失效必须立即发生。此外,您不能相信客户端不会保留和使用旧令牌的副本,无论是否是出于恶意。

因此:

我认为来自@mat -way的答案,#2 TokenBlackList,将是向基于JWT的身份验证添加所需状态的最有效方法。

您有一个黑名单保存这些令牌,直到它们的过期日期。与用户总数相比,代币列表将非常小,因为它只需要保留黑名单上的代币直到到期。我将通过在redis、memcached或其他支持设置密钥过期时间的内存数据存储中放置无效的令牌来实现。

对于每个通过初始JWT身份验证的身份验证请求,仍然需要调用内存中的数据库,但不必将整个用户集的密钥存储在其中。(对于一个特定的网站来说,这可能是也可能不是什么大问题。)

没有使用jwt的刷新…

我想到了两种袭击的场景。一个是关于登录凭证的泄露。另一个是对JWT的盗窃。

For compromised login credentials, when a new login happens, normally send the user an email notification. So, if the customer doesn't consent to being the one who logged in, they should be advised to do a reset of credentials, which should save to database/cache the date-time the password was last set (and set this too when user sets password during initial registration). Whenever a user action is being authorized, the date-time a user changed their password should be fetched from database/cache and compared to the date-time a given JWT was generated, and forbid the action for JWTs that were generated before the said date-time of credentials reset, hence essentially rendering such JWTs useless. That means save the date-time of generation of a JWT as a claim in the JWT itself. In ASP.NET Core, a policy/requirement can be used to do do this comparison, and on failure, the client is forbidden. This consequently logs out the user on the backend, globally, whenever a reset of credentials is done.

For actual theft of JWT... A theft of JWT is not easy to detect but a JWT that expires easily solves this. But what can be done to stop the attacker before the JWT expires? It is with an actual global logout. It is similar to what was described above for credentials reset. For this, normally save on database/cache the date-time a user initiated a global logout, and on authorizing a user action, get it and compare it to the date-time of generation of a given JWT too, and forbid the action for JWTs that were generated before the said date-time of global logout, hence essentially rendering such JWTs useless. This can be done using a policy/requirement in ASP.NET Core, as previously described.

现在,你如何发现JWT被盗?目前我对此的回答是,偶尔提醒用户全局注销并重新登录,因为这肯定会让攻击者注销。

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

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

If you are using axios or a similar promise-based http request lib you can simply destroy token on the front-end inside the .then() part. It will be launched in the response .then() part after user executes this function (result code from the server endpoint must be ok, 200). After user clicks this route while searching for data, if database field user_enabled is false it will trigger destroying token and user will immediately be logged-off and stopped from accessing protected routes/pages. We don't have to await for token to expire while user is permanently logged on.

function searchForData() {   // front-end js function, user searches for the data
    // protected route, token that is sent along http request for verification
    var validToken = 'Bearer ' + whereYouStoredToken; // token stored in the browser 

    // route will trigger destroying token when user clicks and executes this func
    axios.post('/my-data', {headers: {'Authorization': validToken}})
     .then((response) => {
   // If Admin set user_enabled in the db as false, we destroy token in the browser localStorage
       if (response.data.user_enabled === false) {  // user_enabled is field in the db
           window.localStorage.clear();  // we destroy token and other credentials
       }  
    });
     .catch((e) => {
       console.log(e);
    });
}

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.