关于php:为忘记密码生成随机令牌的最佳实践

best practice to generate random token for forgot password

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

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

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


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

因此,代码如下:

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

更新:此答案的以前版本指的是uniqid(),如果存在安全性问题而不仅仅是唯一性问题,那么这是不正确的。uniqid()基本上就是microtime()加上一些编码。有一些简单的方法可以在您的服务器上准确预测microtime()。攻击者可以发出密码重置请求,然后尝试使用一些可能的令牌。如果使用更多的熵,这也是可能的,因为额外的熵同样很弱。感谢@nikic和@scottarciszewski指出了这一点。

有关详细信息,请参阅

  • http://phpsecurity.readthedocs.org/en/latest/insufficient-entropy-for-random-values.html


这将回答"最佳随机"请求:

来自security.stackexchange的adi answer1有一个解决方案:

Make sure you have OpenSSL support, and you'll never go wrong with this one-liner

1
$token = bin2hex(openssl_random_pseudo_bytes(16));

>1。ADI,2018年11月12日,周一,Celeritas,"为确认电子邮件生成不可使用的令牌",2013年9月20日7:06,https://security.stackexchange.com/a/40314/>


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

  • mt_rand()是可预测的(并且只加上31位的熵)
  • uniqid()只增加了29位熵
  • md5()不加熵,它只是确定地混合熵。

因为56位des键可以在大约24小时内被强制执行,平均情况下会有大约59位的熵,所以我们可以计算出2^59/2^56=大约8天。根据令牌验证的实现方式,实际上可能会泄漏定时信息并推断有效重置令牌的前N个字节。

因为这个问题是关于"最佳实践"的,并以…

I want to generate identifier for forgot password

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

使用CSPRNG

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

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

或者,如果安装了ext/mcrypt,则选择bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))。另一个很好的单内衬是bin2hex(openssl_random_pseudo_bytes($n))

将查找与验证程序分离

从我以前在PHP中关于安全"记住我"cookie的工作中,缓解前面提到的时间泄漏(通常由数据库查询引入)的唯一有效方法是将查找与验证分开。

如果您的表看起来像这样(mysql)…

1
2
3
4
5
6
7
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,如下所示:

1
2
3
4
5
6
7
8
9
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(或5.6,随机兼容)中生成重置令牌:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$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')
]);

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

1
2
3
4
5
6
7
8
9
10
$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.
}

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


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

1
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));


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

1
2
3
<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>

你可以使用

1
echo str_shuffle('ASGDHFfdgfdre5475433fd');