已接受答案的早期版本 (md5(uniqid(mt_rand(), true))) 不安全,仅提供大约 2^60 种可能的输出——对于低预算的攻击者来说,在大约一周的时间内进行暴力搜索的范围内:
由于56-bit DES key can be brute-forced in about 24 hours,平均情况下大约有 59 位熵,我们可以计算 2^59 / 2^56 = 大约 8 天。取决于此令牌验证的实现方式,it might be possible to practically leak timing information and infer the first N bytes of a valid reset token。
由于问题是关于“最佳实践”的,并且以...开头。
我想生成忘记密码的标识符
...我们可以推断此令牌具有隐含的安全要求。当您向随机数生成器添加安全要求时,最佳做法是始终使用加密安全的伪随机数生成器(缩写为 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))。
从验证器中分离查找
从我之前在 secure "remember me" cookies in PHP 上的工作来看,减轻上述时间泄漏(通常由数据库查询引入)的唯一有效方法是将查找与验证分开。
如果你的表看起来像这样(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() 存储在数据库中的令牌。
示例代码
在 PHP 7(或带有 random_compat 的 5.6)中使用 PDO 生成重置令牌:
$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.
}
这些代码 sn-ps 不是完整的解决方案(我避开了输入验证和框架集成),但它们应该作为如何做的示例。