如何正确创建密码重置令牌?

信息安全 密码 重设密码
2021-08-15 12:21:51

我搜索了很长时间,但从未真正找到任何完全解释如何创建密码重置令牌的文章或帖子。

到目前为止我所知道的:

  • 它应该是随机的。
  • 它的哈希值应该存储在数据库中。
  • 它应该在一段时间后过期。

我不太确定的是:

  1. 我应该使用加密方法来创建随机令牌吗?Nodecrypto.randomBytes够用吗
  2. 我应该使用加密方法(例如 bcrypt)来散列令牌吗?
  3. 我应该在令牌中包含某些信息(即到期时间、UUID 等)吗?如果是,JWT 是一个好方法吗?

我想整个过程看起来像这样:

  1. 创建一个随机令牌。
  2. 散列令牌并将散列存储在数据库中。
  3. 将令牌以纯文本形式发送给客户端。
  4. 一旦使用,或者超过一定时间没有使用,就将其从数据库中删除。
  5. 使用令牌时,检查其哈希是否与数据库中的任何匹配。
  6. 如果匹配,则允许用户重置密码。否则,禁止该过程。
3个回答

密码重置令牌与密码不太一样。它们是短命的、一次性的,而且——在这里最相关的是——机器生成的和不可记忆的。

  1. 您绝对必须使用加密安全的随机数生成器来生成令牌。crypto.randomBytes足够好,是的。确保令牌足够长;大约 16 个字节(128 位)的东西应该可以工作。
  2. 因为“原像”(输入散列函数的值,在这种情况下是令牌)是随机的,所以不需要使用慢速散列。慢速散列用于蛮力保护,因为密码很烂而且通常不会花很长时间(以机器术语)来猜测;对于该术语的任何实际含义,128 位随机值都不是暴力破解的。只需执行一轮 SHA-256 或类似的。
  3. 馊主意; 它只会使事情复杂化。而是将所有这些东西存储在数据库中。是的,您可以通过将其放入 JWT 来无状态地执行此操作,但 DB 更有意义:
    • 无论如何,您都需要与数据库交互以重置密码。
    • 无论如何,您都需要绕过任何缓存;即使你有一个分布式系统,他们都需要看到这种变化,所以让它无状态是没有意义的。
    • 您只需要进行一次 DB 读取即可验证令牌(其值正确且尚未过期,可以在单个查询中完成),这是一个相对罕见的事件;这不像需要对每个请求进行会话令牌查找。
    • JWT 已过期,但没有方便的方法使它们一次性使用(您必须在服务器上存储“这些 JWT 不再有效”的状态,直到每个 JWT 都过期,这有点错过了 JWT。)密码重置令牌存储在数据库中的可以(并且应该)通过在验证时删除它们来一次性使用。
    • 如果需要,您可以执行清理任务并定期删除过期令牌(从未使用过的令牌)。但是,它们不会在数据库中占用太多空间(如果您只希望每个用户一次有效,则为每个用户提供一个哈希摘要和一个时间戳,或者一个哈希摘要、一个时间戳和一个外键如果您想允许多个令牌对用户一次有效,请使用 Users 表;两者都有优点和缺点)。
    • 与仅检查相比,JWT 具有更多潜在漏洞(有人窃取了您的签名密钥,有人发现密钥是不安全地生成的并且自错误修复以来没有轮换过,您的 JWT 库容易受到密钥或算法混淆等)数据库(我猜除了 SQLi 之外基本上什么都容易受到攻击,希望你知道如何避免)。

你的基本过程是有道理的。

1. 是的,您应该使用加密方法生成令牌。每当您做任何与安​​全相关的事情时,请始终使用加密随机性。是的,crypto.randomBytes很好。使用 16 个字节(您可以少用一点,但不要冒险,除非绝对需要输入令牌而不是单击或复制粘贴)。(16 个字节转换为 24 个 Base64 字符或 32 个十六进制数字。)

2. 使用 SHA-256 或 SHA-512 等加密散列对令牌进行散列。您在这里不需要密码哈希:密码哈希(例如 bcrypt)用于输入是人类记住的密码时,并且当输入是随机生成的足够少的字符串时无用。密码散列有点难使用,而且速度慢得多,在这里你不需要一个。

3. 我认为在令牌中添加信息没有任何优势。无论如何,诸如到期日期和令牌用途等信息都需要在数据库中。

其他两个答案似乎表达了一种关于使用 HMAC 签名令牌进行密码重置的 FUD。

如果正如其他答案所述,担心是关于从服务器窃取您的唱歌密钥,那么攻击者已经可以访问的不仅仅是签名密钥,此时这个论点是没有意义的。

将令牌数据保存在数据库中并不是比使用签名令牌更好(或更差)的解决方案。两者都是合理的方法并且同样安全。让这个答案成为硬币的另一面。

Django Web 框架使用这种方法。它是最常用的 Web 框架之一,因此我们可以放心,这种方法非常安全。

Django 使用以下参数生成令牌:

  1. 用户标识
  2. 当前时间戳这对于了解令牌的年龄很有用。
  3. 当前密码的哈希值如果用户生成许多重置令牌并使用一个令牌重置其密码,则数据库中的哈希值将更改,但所有令牌仍具有旧哈希值,因此所有令牌将自动失效。
  4. 上次登录的时间戳如果用户生成了重置令牌,但后来​​登录了他们的帐户,则此时间戳将在数据库中更新,但令牌仍具有旧时间戳,因此令牌将自动失效。

与 JWT 不同,最终令牌在纯文本中没有任何这些参数。仅发送令牌的十六进制摘要。

那么,如果令牌没有发送数据,你怎么知道这个令牌属于哪个用户呢?

有两种方法可以将令牌与用户相关联。

第一:可以用token发送用户的id,如:<user_id>:<the_token_value>.

现在,当用户单击链接时,您可以从令牌中读取用户 id,使用之前的参数重新计算哈希,并将此哈希[参见下面的注释]与颁发的令牌进行比较。

如果它们匹配,则您允许用户重置密码。

第二: Django 处理这个问题的方式是它不发送带有令牌的用户 id,而是在重置 url 中。所以 Django 生成的重置 url 看起来像这样/reset-password/<user_id>/<token>

当用户点击链接时,您可以从 url 中读取用户 id,然后使用之前的参数重新计算 hash,并将这个 hash [参见下面的注释]与颁发的令牌进行比较。


重要的提示:

请不要像比较字符串那样比较哈希(即使用相等运算符)。这样比较容易受到定时攻击

在 Python 中,有一个secrets.compare_digest函数可以解决这个问题。请找到您的语言中的等价物来比较哈希值。