首页 文章

使JSON Web令牌无效

提问于
浏览
268

对于我正在研究的新node.js项目,我正在考虑从基于cookie的会话方法切换(我的意思是,将id存储到包含用户浏览器中的用户会话的键值存储)使用JSON Web令牌(jwt)进行基于令牌的会话方法(无键值存储) .

该项目是一个利用socket.io的游戏 - 在一个会话中将有多个通信通道(web和socket.io)的情况下,基于令牌的会话将非常有用 .

如何使用jwt方法从服务器提供令牌/会话失效?

我还想了解在这种范例中我应该注意哪些常见(或不常见)的陷阱/攻击 . 例如,如果此范例容易受到与基于会话存储/ cookie的方法相同/不同类型的攻击 .

所以,说我有以下内容(改编自thisthis):

会话商店登录:

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数据库 .

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

16 回答

  • 1

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

    1)只需从客户端删除令牌

    显然,这对服务器端安全性没有任何作用,但它确实通过删除令牌来阻止攻击者(即,他们必须在注销之前窃取令牌) .

    2)创建令牌黑名单

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

    3)只需保持令牌到期时间短并经常旋转它们

    如果您以足够短的间隔保持令牌到期时间,并让正在运行的客户端跟踪并在必要时请求更新,则数字1将有效地用作完整的注销系统 . 此方法的问题在于,它使得用户无法在关闭客户端代码之间保持登录(取决于您到达时间间隔的时间长度) .

    临时计划

    如果发生紧急情况或用户令牌遭到入侵,您可以做的一件事是允许用户使用其登录凭据更改基础用户查找ID . 这将使所有关联的令牌无效,因为将无法再找到关联的用户 .

    我还想要注意,最后一个登录日期包含令牌是一个好主意,这样您就可以在一段遥远的时间后强制执行重新登录 .

    就使用令牌的攻击的相似性/差异而言,这篇文章解决了这个问题:http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/

  • 2

    上面发布的想法很好,但是使所有现有JWT无效的一种非常简单易行的方法就是改变秘密 .

    如果您的服务器创建了JWT,请使用机密(JWS)对其进行签名,然后将其发送给客户端,只需更改密钥即可使所有现有令牌无效,并要求所有用户获取新令牌以进行身份验证,因为旧令牌突然变为无效到服务器 .

    它不需要对实际令牌内容(或查找ID)进行任何修改 .

    显然,这仅适用于您希望所有现有令牌到期的紧急情况,对于每个令牌到期,需要上述解决方案之一(例如短令牌到期时间或使令牌内存储的密钥无效) .

  • 35

    这主要是一篇长篇评论,支持并 Build 在@mattway的答案之上

    鉴于:

    此页面上的其他一些建议的解决方案主张在每个请求上访问数据存储区 . 如果您点击主数据存储区以验证每个身份验证请求,那么我认为使用JWT而不是其他已 Build 的令牌身份验证机制的理由更少 . 如果你每次都去数据存储区,你基本上使JWT成为有状态,而不是无状态 .

    (如果您的站点收到大量未经授权的请求,那么JWT会拒绝它们而不会访问数据存储区,这很有帮助 . 可能还有其他用例 . )

    鉴于:

    对于典型的真实世界Web应用程序,无法实现真正无状态的JWT身份验证,因为无状态JWT无法为以下重要用例提供 immediatesecure 支持:

    用户的帐户被删除/阻止/暂停 .

    用户密码已更改 .

    用户的角色或权限已更改 .

    用户已注销由管理员 .

    站点管理员更改JWT令牌中的任何其他应用程序关键数据 .

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

    因此:我认为@ matt-way,#2 TokenBlackList的答案是将所需状态添加到基于JWT的身份验证的最有效方法 .

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

    您仍然必须为每个通过初始JWT身份验证的身份验证请求调用内存数据库,但您不必为其中的整个用户组存储密钥 . (对于给定的站点,这可能是也可能不是什么大问题 . )

  • 233

    我会在用户模型上记录jwt版本号 . 新的jwt令牌会将其版本设置为此 .

    验证jwt时,只需检查它的版本号是否等于用户当前的jwt版本 .

    任何时候你想要使旧jwts无效,只需碰撞用户jwt版本号 .

  • 5

    还没有尝试过,它基于其他一些答案使用了很多信息 . 这里的复杂性是避免每次请求用户信息的服务器端数据存储调用 . 大多数其他解决方案需要对用户会话存储的每个请求进行数据库查找 . 在某些情况下这很好但是这是为了避免这种调用而创建的,并且使得所需的服务器端状态非常小 . 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:

    Goals:

    • 缓解数据存储的使用(无状态) .

    • 能够强制注销所有用户 .

    • 能够随时强制退出任何个人 .

    • 能够在一定时间后要求重新输入密码 .

    • 能够与多个客户合作 .

    • 能够在用户单击特定客户端的注销时强制重新登录 . (为防止用户离开后有人"un-deleting"客户端令牌 - 请参阅注释以获取更多信息)

    The Solution:

    • 使用短期(<5米)访问令牌与存储时间较长(几小时)的客户端配对refresh-token .

    • 每个请求都会检查auth或刷新令牌的有效期限 .

    • 当访问令牌过期时,客户端使用刷新令牌刷新访问令牌 .

    • 在刷新令牌检查期间,服务器检查用户ID的小黑名单 - 如果找到则拒绝刷新请求 .

    • 当客户端没有有效(未过期)刷新或身份验证令牌时,用户必须重新登录,因为所有其他请求都将被拒绝 .

    • 在登录请求中,检查用户数据存储是否禁止 .

    • 注销时 - 将该用户添加到会话黑名单中,以便他们必须重新登录 . 您必须存储其他信息,以便不在多设备环境中将其记录到所有设备之外,但可以通过添加设备字段来完成用户黑名单 .

    • 在x个时间后强制重新进入 - 在auth令牌中维护上次登录日期,并根据请求进行检查 .

    • 强制注销所有用户 - 重置令牌哈希键 .

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

    Cons:

    • 仍然需要对刷新令牌请求执行数据存储查找 .

    • 无效的令牌可能会继续为访问令牌的TTL操作 .

    Pros:

    • 提供所需的功能 .

    • 刷新令牌操作在正常操作下对用户隐藏 .

    • 仅需要对刷新请求而不是每个请求执行数据存储查找 . 即每15分钟1次,而不是每秒1次 .

    • 将服务器端状态最小化为非常小的黑名单 .

    使用此解决方案,不需要像内存一样的内存数据存储,至少不需要用户信息,因为服务器每15分钟左右只进行一次数据库调用 . 如果使用reddis,那么在那里存储有效/无效的会话列表将是一个非常快速和简单的解决方案 . 无需刷新令牌 . 每个身份验证令牌都有一个会话ID和设备ID,它们可以在创建时存储在reddis表中,并在适当时使其无效 . 然后,他们将在每个请求上进行检查,并在无效时被拒绝 .

  • 8

    我一直在考虑的方法是在JWT中始终具有 iat (发布于)值 . 然后,当用户注销时,将该时间戳存储在用户上记录 . 验证JWT时,只需将 iat 与上次注销的时间戳进行比较 . 如果 iat 较旧,那么如果JWT无效,它始终会拉动用户记录 .

    我看到的主要缺点是,如果它们在多个浏览器中,或者也有移动客户端,它会将它们记录在所有会话中 .

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

  • 19

    我在这里有点晚了,但我想我有一个不错的解决方案 .

    我的数据库中有一个“last_password_change”列,用于存储上次更改密码的日期和时间 . 我还将发行日期/时间存储在JWT中 . 在验证令牌时,我会检查在发出令牌后密码是否已更改,以及令牌是否被拒绝,即使它尚未过期 .

  • 64

    您可以在用户的文档/记录上的数据库中包含“last_key_used”字段 .

    当用户使用user登录并传递时,生成新的随机字符串,将其存储在last_key_used字段中,并在签名时将其添加到有效负载中 .

    当用户使用令牌登录时,请检查DB中的last_key_used以匹配令牌中的last_key_used .

    然后,当用户执行注销时,或者如果要使令牌无效时,只需将“last_key_used”字段更改为另一个随机值,任何后续检查都将失败,从而强制用户使用用户登录并再次传递 .

  • 1
    • 给予代币1天的到期时间

    • 维护每日黑名单 .

    • 将无效/注销令牌放入黑名单

    对于令牌验证,首先检查令牌到期时间,如果令牌未过期,则检查黑名单 .

    对于长会话需求,应该有一种延长令牌到期时间的机制 .

  • 2

    在参加晚会之后,经过一些研究,我得到了两分钱 . 在注销期间,请确保发生以下事情......

    清除客户端存储/会话

    无论何时分别发生登录或注销,都要更新用户表的上次登录日期时间和注销日期时间 . 所以登录日期时间总是应该大于logout(或者如果当前状态是登录但尚未注销,则保持注销日期为null)

    这比保持额外的黑名单表和定期清除要简单得多 . 多设备支持需要额外的表来保持登录,注销日期以及一些其他详细信息,如操作系统或客户端详细信息 .

  • -1

    为什么不直接使用jti声明(nonce)并将其作为用户记录字段存储在列表中(db依赖,但至少以逗号分隔列表是好的)?不需要单独查找,因为其他人已经指出你可能想要获取用户记录,这样你就可以为不同的客户端实例拥有多个有效令牌(“logout everywhere”可以将列表重置为空)

  • 47

    我是通过以下方式做到的:

    • 生成 unique hash ,然后将其存储在 redisJWT 中 . 这可以称为 session

    • 我们还将存储特定 JWTrequests 的数量 - 每次将jwt发送到服务器时,我们都会递增 requests 整数 . (这是可选的)

    因此,当用户登录时,会创建一个唯一的哈希值,存储在redis中并注入到 JWT 中 .

    当用户尝试访问受保护的 endpoints 时,您将从 JWT 获取唯一会话哈希,查询redis并查看它是否匹配!

    我们可以从此扩展并使我们的 JWT 更加安全,具体如下:

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

    这意味着 JWT 会不断变化并停止被盗,被盗或其他东西 .

  • 11

    保留这样的内存列表

    user_id   revoke_tokens_issued_before
    -------------------------------------
    123       2018-07-02T15:55:33
    567       2018-07-01T12:34:21
    

    如果您的令牌在一周内到期,则清理或忽略早于此的记录 . 同时保留每个用户的最新记录 . 列表的大小取决于您保留令牌的时间以及用户撤销令牌的频率 . 仅在表更改时使用db . 应用程序启动时将表加载到内存中 .

  • 0

    每个用户字符串唯一,并且全局字符串散列在一起

    作为JWT秘密部分,允许个人和全局令牌失效 . 在请求身份验证期间以db查找/读取为代价的最大灵活性 . 也很容易缓存,因为它们很少变化 .

  • -1

    如果您只是使用expiresIn:0从服务器生成一个新令牌并将其返回给客户端并将其存储在cookie中,该怎么办?

  • 1

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

    我认为这不是最好的解决方案,但这对我有用 .

相关问题