99

我想为忘记密码生成标识符。我读到我可以通过使用带有 mt_rand() 的时间戳来做到这一点,但有些人说时间戳可能不是每次都是唯一的。所以我在这里有点困惑。我可以通过使用时间戳来做到这一点吗?

问题
生成自定义长度的随机/唯一令牌的最佳做法是什么?

我知道这里有很多问题,但是在阅读了不同人的不同意见后,我变得更加困惑。

4

5 回答 5

156

在 PHP 中,使用random_bytes(). 原因:您正在寻找获取密码提醒令牌的方法,并且,如果它是一次性登录凭据,那么您实际上有一个要保护的数据(即 - 整个用户帐户)

因此,代码如下:

//$length = 78 etc
$token = bin2hex(random_bytes($length));

更新:这个答案的先前版本指的uniqid()是,如果存在安全问题而不仅仅是唯一性,这是不正确的。uniqid()本质上只是microtime()一些编码。有一些简单的方法可以microtime()在您的服务器上获得准确的预测。攻击者可以发出密码重置请求,然后尝试通过几个可能的令牌。如果使用 more_entropy,这也是可能的,因为额外的熵同样很弱。感谢@NikiC@ScottArciszewski指出这一点。

有关更多详细信息,请参阅

于 2013-09-20T07:14:17.107 回答
73

这回答了“最佳随机”请求:

来自 Security.StackExchange 的Adi 的回答1对此有一个解决方案:

确保你有 OpenSSL 支持,你永远不会出错这个单线

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi,2018 年 11 月 12 日星期一,Celeritas,“为确认电子邮件生成不可猜测的令牌”,2013 年 9 月 20 日 7:06,https: //security.stackexchange.com/a/40314/

于 2015-03-19T05:17:32.583 回答
56

较早版本的已接受答案 ( md5(uniqid(mt_rand(), true))) 不安全,仅提供大约 2^60 种可能的输出——在低预算攻击者大约一周时间内的暴力搜索范围内:

由于56 位 DES 密钥可以在大约 24 小时内被暴力破解,并且平均情况下大约有 59 位熵,我们可以计算 2^59 / 2^56 = 大约 8 天。根据令牌验证的实现方式,实际上可能会泄漏时序信息并推断出有效重置令牌的前 N ​​个字节

由于问题是关于“最佳实践”并以...开头

我想为忘记密码生成标识符

...我们可以推断该令牌具有隐含的安全要求。当您向随机数生成器添加安全要求时,最佳实践是始终使用加密安全的伪随机数生成器(缩写为 CSPRNG)。


使用 CSPRNG

在 PHP 7 中,您可以使用bin2hex(random_bytes($n))(其中$n是大于 15 的整数)。

在 PHP 5 中,您可以使用random_compat来公开相同的 API。

或者,bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))如果您已ext/mcrypt安装。另一个很好的单线是bin2hex(openssl_random_pseudo_bytes($n)).

将 Lookup 与 Validator 分开

从我之前关于PHP 中安全“记住我”cookie 的工作中提取,减轻上述时间泄漏(通常由数据库查询引入)的唯一有效方法是将查找与验证分开。

如果您的表看起来像这样(MySQL)...

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id)
);

...您需要再添加一列,selector,如下所示:

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    selector CHAR(16),
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id),
    KEY(selector)
);

使用 CSPRNG 发出密码重置令牌时,将两个值都发送给用户,将选择器和随机令牌的 SHA-256 散列存储在数据库中。使用选择器获取哈希值和用户 ID,计算用户提供的令牌的 SHA-256 哈希值以及存储在数据库中的令牌hash_equals()

示例代码

使用 PDO 在 PHP 7(或带有 random_compat 的 5.6)中生成重置令牌:

$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);

$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
    'selector' => $selector,
    'validator' => bin2hex($token)
]);

$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour

$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
    'userid' => $userId, // define this elsewhere!
    'selector' => $selector,
    'token' => hash('sha256', $token),
    'expires' => $expires->format('Y-m-d\TH:i:s')
]);

验证用户提供的重置令牌:

$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
    $calc = hash('sha256', hex2bin($validator));
    if (hash_equals($calc, $results[0]['token'])) {
        // The reset token is valid. Authenticate the user.
    }
    // Remove the token from the DB regardless of success or failure.
}

这些代码片段不是完整的解决方案(我避开了输入验证和框架集成),但它们应该作为如何做的示例。

于 2015-07-14T23:47:32.177 回答
7

您还可以使用 DEV_RANDOM,其中 128 = 1/2 生成的令牌长度。下面的代码生成 256 个令牌。

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
于 2013-11-21T18:13:58.867 回答
2

每当您需要非常随机的令牌时,这可能会有所帮助

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
于 2017-05-22T11:22:40.970 回答