我想生成忘记密码的标识符。我读到我可以通过将时间戳与mt_rand()一起使用来做到这一点,但有人说时间戳记并非每次都唯一。所以我有点困惑。我可以在此使用时间戳吗?
问题
生成自定义长度的随机/唯一令牌的最佳实践是什么?
我知道这里有很多问题,但是在听取了不同人的不同意见后,我变得更加困惑。
我想生成忘记密码的标识符。我读到我可以通过将时间戳与mt_rand()一起使用来做到这一点,但有人说时间戳记并非每次都唯一。所以我有点困惑。我可以在此使用时间戳吗?
问题
生成自定义长度的随机/唯一令牌的最佳实践是什么?
我知道这里有很多问题,但是在听取了不同人的不同意见后,我变得更加困惑。
Answers:
在PHP中,使用random_bytes()
。原因:您正在寻找一种获取密码提醒令牌的方法,并且,如果它是一次性登录凭据,则实际上您有一个数据需要保护(即-整个用户帐户)
因此,代码如下:
//$length = 78 etc
$token = bin2hex(random_bytes($length));
更新:此答案的先前版本所指,uniqid()
如果存在安全问题,而不仅仅是唯一性,那是不正确的。uniqid()
本质上只是microtime()
带有一些编码。有一些简单的方法可以microtime()
对您的服务器进行准确的预测。攻击者可以发出密码重置请求,然后尝试几个可能的令牌。如果使用more_entropy,这也是可能的,因为附加熵同样很弱。感谢@NikiC和@ScottArciszewski指出这一点。
有关更多详细信息,请参见
random_bytes()
仅从PHP7开始可用。对于较旧的版本,@ yesitsme的答案似乎是最好的选择。
$length
什么?用户的ID?或者是什么?
这回答了“最佳随机”请求:
Adi来自Security.StackExchange 的答案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/
openssl_random_pseudo_bytes($length)
-支持:PHP 5> = 5.3.0,....................................... ...................(对于PHP 7及更高版本,请使用random_bytes($length)
)............................... ....................(对于低于5.3的
可接受的答案(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。
或者,bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
如果已ext/mcrypt
安装。另一个好单线是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,使用计算用户提供的令牌与数据库中存储的令牌的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.
}
这些代码段不是完整的解决方案(我避开了输入验证和框架集成),但是它们应作为操作的示例。
hash('sha256', bin2hex($token))
; 2)使用进行验证if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...
?谢谢!
id
作为选择器?我的意思是account_recovery
表的主键。选择器不需要额外的安全层,对吗?谢谢!
id:secret
还可以 selector:secret
还可以 secret
本身不是。目的是将数据库查询(定时泄漏)与身份验证协议(应为固定时间)分开。
openssl_random_pseudo_bytes
替代项会有什么危害random_bytes
吗?另外,是否不应该在链接的查询字符串中仅附加选择器而不附加验证器?
您还可以使用DEV_RANDOM,其中128 =生成的令牌长度的1/2。下面的代码生成256个令牌。
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
MCRYPT_DEV_URANDOM
结束MCRYPT_DEV_RANDOM
。