PHP random string generator: как работает генератор случайных строк под капотом
Каждый разработчик хотя бы раз сталкивался с задачей: сгенерировать случайную строку. Для токенов, временных паролей, ID сессий или просто для тестовых данных. В PHP есть с десяток способов это сделать, но какой из них правильный? И главное — почему?
На Stack Overflow вопрос «PHP random string generator» набрал тысячи просмотров и сотни ответов. Но большинство из них — это просто готовые сниппеты без объяснения. Сегодня мы заглянем под капот PHP и разберёмся, как на самом деле работают генераторы случайных строк, чем отличается rand() от random_int() и почему ваш «случайный» пароль может быть совсем не случайным.
Что такое «случайность» в PHP?
В программировании случайность бывает двух видов: псевдослучайность и криптостойкая случайность. Разница между ними — как между игральной костью и предсказуемым калькулятором.
Псевдослучайные числа (PRNG) генерируются алгоритмом на основе начального зерна (seed). Если вы знаете зерно — вы знаете всю последовательность. Для простых задач (рандомизация цвета в дизайне, тестовые данные) этого достаточно. Но для токенов аутентификации и паролей — это катастрофа.
PHP предлагает несколько функций, и каждая из них — это разный уровень «случайности»:
rand()— классический PRNG, быстрый, но предсказуемыйmt_rand()— улучшенный алгоритм Mersenne Twister, быстрее и с большим периодомrandom_int()иrandom_bytes()— криптостойкие генераторы (PHP 7+)openssl_random_pseudo_bytes()— ещё один криптостойкий вариант
Как работает самый популярный сниппет?
Вот классический код, который гуляет по интернету:
<?php
function generateRandomString($length = 10) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
?>
На первый взгляд — всё логично. Берём строку с символами, случайно выбираем индекс, повторяем. Но давайте разберём, что здесь происходит на самом деле.
Проблема №1: rand() и его зерно
Функция rand() в PHP по умолчанию использует алгоритм Linear Congruential Generator (LCG). Это один из самых старых и самых предсказуемых методов. Если вы не вызвали srand() с случайным зерном, PHP автоматически инициализирует генератор текущим временем (в микросекундах).
Зная точное время запуска скрипта (например, из логов сервера), злоумышленник может воспроизвести всю последовательность rand(). Для токена восстановления пароля — это прямой путь к взлому.
Проблема №2: Смещение вероятности (Bias)
Даже если не думать о безопасности, есть математическая проблема. Представьте, что rand(0, 61) (62 символа в строке). PHP внутренне использует диапазон от 0 до getrandmax() (обычно 2147483647). 2147483647 не делится нацело на 62. Остаток от деления — это 31. Это значит, что первые 31 число из диапазона имеют чуть большую вероятность выпасть, чем остальные.
На практике разница мизерная, но если вам нужна равномерно распределённая случайная строка, rand() — не лучший выбор. random_int() решает эту проблему, используя алгоритм, который гарантирует равномерное распределение.
Криптостойкий генератор: что под капотом?
В PHP 7 появились функции random_int() и random_bytes(). Они используют системный источник энтропии:
- На Linux —
/dev/urandom(илиgetrandom()системный вызов) - На Windows —
CryptGenRandom() - На macOS —
SecRandomCopyBytes()
Эти источники собирают «шум» из физического мира: движения мыши, нажатия клавиш, сетевые пакеты, задержки диска. Это настоящая случайность, которую невозможно предсказать.
Когда вы вызываете random_bytes(16), PHP просит у операционной системы 16 байт случайных данных. Операционная система собирает энтропию из пула ядра и возвращает результат. Это медленнее, чем rand(), но для токенов безопасности — это единственно верный путь.
Пишем правильный генератор случайных строк
Теперь, когда мы понимаем теорию, напишем безопасный и эффективный генератор. Вот версия, которую я рекомендую для продакшена:
<?php
function secureRandomString(int $length = 16): string {
// Используем криптостойкий источник случайности
$bytes = random_bytes($length);
// Преобразуем байты в шестнадцатеричную строку
return bin2hex($bytes);
}
?>
Этот код генерирует строку длиной $length * 2 символов (каждый байт — два hex-символа). Для токена длиной 32 символа (128 бит энтропии) это более чем достаточно.
Но что если нужна строка из букв и цифр, а не только hex? Вот универсальный генератор:
<?php
function alphanumericRandomString(int $length = 16): string {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
// random_int гарантирует равномерное распределение
$randomString .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomString;
}
?>
Обратите внимание: мы используем random_int() вместо rand(). Это устраняет обе проблемы — и предсказуемость, и смещение вероятности.
Сравнение производительности: rand vs random_int
Многие избегают random_int() из-за страха, что это медленно. Давайте проверим. Напишем простой бенчмарк:
<?php
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
rand(0, 61);
}
echo "rand: " . (microtime(true) - $start) . " сек\n";
$