对于我正在从事的一个新的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上有一个“last_key_used”字段。

当用户使用user和pass登录时,生成一个新的随机字符串,将其存储在last_key_used字段中,并在签名令牌时将其添加到负载中。

当用户使用令牌登录时,检查数据库中的last_key_used是否与令牌中的last_key_used匹配。

然后,当用户执行注销操作时,或者如果您想使令牌失效,只需将“last_key_used”字段更改为另一个随机值,随后的任何检查都将失败,从而迫使用户使用user登录并再次通过。

其他回答

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

让“立即”表示“~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”范围内。 如果在“即时”持续时间内有来自客户端的大量请求,那么这是最有益的。

A good approach to invalidating a token would still need database trips. For a purpose that includes when some parts of the user record change, for example changing roles, changing passwords, email, and more. One can add a modified or updated_at field in the user record, which records the time of this change, and then you include this in the claims. So when a JWT is authenticated, you compare the time in the claims with the one recorded in the DB, if that of the claim was before, then token is invalid. This approach is also similar to storing the iat in the DB.

注意:如果您正在使用modified或updated_at选项,那么您还必须在用户登录和退出时更新它。

IAM解决方案,如Keycloak(我曾经工作过)提供令牌撤销端点

令牌撤销端点 /领域/{域名}/协议/ openid-connect /撤销

如果只是想注销用户代理(或用户),也可以调用端点(这将使令牌无效)。同样,在Keycloak的情况下,依赖方只需要调用端点

/realms/{realm-name}/protocol/openid-connect/logout

链接以防万一,如果你想了解更多

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);
    });
}

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

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

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