我想为忘记密码生成标识符。我读过,可以使用时间戳和mt_rand()来实现,但有些人说时间戳可能每次都不唯一。所以我有点困惑。我可以使用时间戳来实现吗?
问题
生成自定义长度的随机/唯一令牌的最佳做法是什么?
我知道这里有很多相关问题,但在阅读不同人的不同意见后,我变得更加困惑了。
我想为忘记密码生成标识符。我读过,可以使用时间戳和mt_rand()来实现,但有些人说时间戳可能每次都不唯一。所以我有点困惑。我可以使用时间戳来实现吗?
问题
生成自定义长度的随机/唯一令牌的最佳做法是什么?
我知道这里有很多相关问题,但在阅读不同人的不同意见后,我变得更加困惑了。
random_bytes()
。原因是:您正在寻找获取密码提醒令牌的方法,如果这是一次性登录凭证,则实际上有需要保护的数据(即 - 整个用户帐户)。//$length = 78 etc
$token = bin2hex(random_bytes($length));
更新: 之前的版本提到了uniqid()
,如果涉及到安全问题并不只是唯一性,则是不正确的。实际上,uniqid()
本质上只是带有一些编码的microtime()
。您可以通过简单的方法来获取服务器上microtime()
的准确预测值。攻击者可以发出密码重置请求,然后尝试几个可能的令牌。如果使用more_entropy,这也是可能的,因为额外的熵同样很弱。感谢@NikiC和@ScottArciszewski指出这一点。
有关更多详细信息,请参见:
$length
应该是什么?用户的 ID 吗?还是其他什么? - stack1461501637330902918203684832716283019655932542976
种不同的可能令牌。我甚至可以说 $length=10; 就足够了。10 将给你 1208925819614629174706176
种可能的令牌;顺便说一下,这个数学是 2**(10*8)
,你可以用 https://www.grc.com/haystack.htm 进行一些猜测难度计算) - hanshenrik这是针对“最佳随机”的请求的答案:
Security.StackExchange上Adi的回答1提供了一个解决方案:
确保您有OpenSSL支持,然后您永远不会错过这个一行命令。
$token = bin2hex(openssl_random_pseudo_bytes(16));
1. Adi, Mon Nov 12 2018, Celeritas, "生成一个不可预测的令牌用于确认电子邮件", Sep 20 '13 at 7:06, https://security.stackexchange.com/a/40314/
openssl_random_pseudo_bytes($length)
- 支持:PHP 5 >= 5.3.0,..........................................................(对于 PHP 7 及以上版本,请使用 random_bytes($length)
).......................................... (对于 PHP 版本低于 5.3 的情况,请不要使用) - jave.web先前的被接受答案版本(md5(uniqid(mt_rand(), true))
)不安全,只提供约2^60种可能性--对于低预算攻击者而言,在大约一周的时间内进行暴力搜索是可以的:
mt_rand()
可预测(只添加了31位熵)uniqid()
只添加了29位熵md5()
不会添加熵,它只是确定性地混合它由于56位DES密钥在大约24小时内就可以被暴力破解,平均情况下的熵为约59位,因此我们可以计算2^59 / 2^56 = 约8天。根据这个令牌验证的实现方式,可能会实际泄漏时间信息并推断出有效重置令牌的前N个字节。
由于问题涉及到“最佳实践”,并且以...
我想生成用于忘记密码的标识符
...我们可以推断出该令牌具有隐含的安全要求。当向随机数生成器添加安全要求时,最佳做法是始终使用密码学安全的伪随机数生成器(缩写为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):
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,在使用hash_equals()
函数计算用户提供的令牌与存储在数据库中的令牌的SHA-256哈希。
使用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.
}
这些代码片段并非完整的解决方案(我没有进行输入验证和框架集成),但它们应该作为如何操作的示例。
hash('sha256', bin2hex($token))
将令牌的哈希十六进制值存储在数据库中,2)使用if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...
进行验证?谢谢! - Guicaraid
作为选择器呢?我的意思是,account_recovery
表的主键。我们不需要为选择器添加额外的安全层,对吧?谢谢! - Andre Polykanineid:secret
是可以的。selector:secret
也是可以的。但是 secret
本身不行。目标是将数据库查询(可能会泄露时间信息)与认证协议(应该是恒定时间的)分离开来。 - Scott Arciszewskiparagonie/random_compat
。 - Scott Arciszewski您也可以使用DEV_RANDOM,其中128 = 生成的令牌长度的1/2。下面的代码生成256个令牌。
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
MCRYPT_DEV_URANDOM
而不是MCRYPT_DEV_RANDOM
。 - Scott Arciszewski当您需要一个非常随机的令牌时,这可能会有所帮助。
<?php
echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>