рекомендуется генерировать случайный токен для забытого пароля
Я хочу сгенерировать идентификатор для забытого пароля. Я читал, что могу сделать это, используя метку времени с помощью mt_rand(), но некоторые люди говорят, что метка времени может не быть уникальной каждый раз. Так что я немного запутался здесь. Могу ли я сделать это, используя отметку времени с этим?
Вопрос
Как лучше всего генерировать случайные/уникальные токены произвольной длины?
Я знаю, что здесь задают много вопросов, но я все больше запутываюсь после прочтения разных мнение разных людей.
6 answers
В PHP используйте random_bytes()
. Причина: вы ищете способ получить токен напоминания пароля, и, если это одноразовые учетные данные для входа, то у вас действительно есть данные для защиты (то есть - вся учетная запись пользователя)
Итак, код будет следующим:
//$length = 78 etc
$token = bin2hex(random_bytes($length));
Обновление: предыдущие версии этого ответа ссылались на uniqid()
, и это неверно, если речь идет о безопасности, а не только об уникальности. uniqid()
по сути, это просто microtime()
с некоторой кодировкой. Существуют простые способы получить точные прогнозы microtime()
на вашем сервере. Злоумышленник может отправить запрос на сброс пароля, а затем попробовать использовать несколько вероятных токенов. Это также возможно, если используется больше энтропии, так как дополнительная энтропия также слаба. Спасибо @NikiC и @Scottarciszewski за указание на это.
Для получения более подробной информации см.
Это лучший случайный ответ
$token = bin2hex(openssl_random_pseudo_bytes(16));
Более ранняя версия принятого ответа (md5(uniqid(mt_rand(), true))
) небезопасна и предлагает только около 2^60 возможных выходов - вполне в пределах досягаемости поиска грубой силы примерно через неделю для малобюджетного злоумышленника:
-
mt_rand()
предсказуемо (и добавляет только до 31 бита энтропии) uniqid()
только добавляет до 29 бит энтропии-
md5()
не добавляет энтропии, он просто детерминированно смешивает ее
Начиная с 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))
.
Отделение поиска от средства проверки
Исходя из моей предыдущей работы над безопасными файлами cookie "запомни меня" в 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 случайного токена в базе данных. Используйте селектор для захвата хэша и идентификатора пользователя, вычислите хэш SHA-256 токена, который пользователь предоставляет вместе с тем, который хранится в базе данных, используя hash_equals()
.
Пример кода
Генерация токена сброса в PHP 7 (или 5.6 с помощью random_compat) с помощью 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.
}
Эти фрагменты кода не являются полными решениями (я отказался от проверки ввода и интеграции фреймворка), но они должны служить примером того, что нужно делать.
Вы также можете использовать DEV_RANDOM, где 128 = 1/2 длины сгенерированного токена. Приведенный ниже код генерирует 256 токенов.
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Это может быть полезно всякий раз, когда вам нужен очень, очень случайный токен
<?php
echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Вы можете использовать
echo str_shuffle('ASGDHFfdgfdre5475433fd');