Эффективное хэширование паролей с помощью password_hash
Основы работы с паролями в PHP
Как правильно хэшировать пароль при регистрации?
Самый безопасный и рекомендуемый способ - использование функций password_hash и password_verify. Они автоматически генерируют случайную соль и используют современный алгоритм bcrypt (или Argon2, если доступен).
$password = 'user_password123';
$hash = password_hash($password, PASSWORD_DEFAULT);
echo $hash; // например, $2y$10$...Пояснение: PASSWORD_DEFAULT использует bcrypt по умолчанию, который считается безопасным. Функция возвращает строку, содержащую соль, алгоритм и стоимость. Эта строка может храниться в базе данных.
Типичные проблемы и ошибки:
- Применение устаревших алгоритмов (MD5, SHA1) - уязвимы для подбора.
- Неправильная настройка стоимости (cost) - слишком низкая делает хэш быстрым для атак, слишком высокая замедляет работу сервера.
- Хранение пароля в открытом виде в логах или при выводе.
- Использование фиксированной соли или соли маленькой длины.
Решение:
- Рекомендуется всегда использовать PASSWORD_DEFAULT или PASSWORD_BCRYPT.
- Устанавливать стоимость (cost) в диапазоне 10-12 для bcrypt, тестируя производительность сервера.
- Проверять наличие PASSWORD_ARGON2ID и применять его на PHP 7.2+ для ещё большей безопасности.
Как проверить пароль при входе?
Для проверки используется password_verify:
$inputPassword = 'user_password123';
$storedHash = '$2y$10$...'; // из базы данных
if (password_verify($inputPassword, $storedHash)) {
echo 'Пароль верен';
} else {
echo 'Неверный пароль';
}Пояснение: функция сравнивает введённый пароль с хэшем, учитывая соль и алгоритм. Нет необходимости извлекать соль отдельно.
Ошибки: передача аргументов в неправильном порядке (хэш должен быть вторым).
Как обновить хэш до более нового алгоритма?
Применяется password_needs_rehash:
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT, ['cost' => 12])) {
$newHash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
// сохранить $newHash в базу данных
}Это позволяет автоматически обновлять хэши при изменении алгоритма или стоимости без участия пользователя.
Как не следует хэшировать пароли? (использование MD5)
Старый способ: md5($password) - крайне небезопасен из-за быстрого вычисления и отсутствия соли.
$hash = md5('password123');Проблемы: атаки по словарю и радужные таблицы. Такой хэш подбирается за секунды. Даже добавление соли не спасает, если соль фиксирована.
Как раньше хэшировали пароли с солью? (SHA1 + соль)
Пример: sha1($salt.$password) - всё ещё слабо, так как SHA1 быстр.
$salt = 'random_fixed_salt';
$hash = sha1($salt . $password);Проблемы: соль фиксирована, алгоритм ASIC-дружелюбен, подбор возможен.
Как использовать crypt() для хэширования?
Функция crypt() с правильной солью (например, '$2y$10$...') может давать bcrypt-хэш, но требует ручного формирования соли.
$salt = '$2y$10$' . bin2hex(random_bytes(22));
$hash = crypt($password, $salt);Ошибки: неправильный формат соли, сложность с управлением параметрами. Лучше применять password_hash.
Как вручную создать безопасный хэш с случайной солью? (hash + random_bytes)
Можно использовать hash('sha256', random_bytes(16) . $password), но это не рекомендуется, так как не имеет настраиваемой стоимости.
$salt = random_bytes(16);
$hash = hash('sha256', $salt . $password);Проблемы: фиксированное время вычисления, слабый по сравнению с bcrypt/argon2.
Как использовать Argon2 для хэширования паролей?
Если PHP 7.2+, применяется PASSWORD_ARGON2ID:
$hash = password_hash($password, PASSWORD_ARGON2ID, ['memory_cost' => 1024, 'time_cost' => 2, 'threads' => 2]);Плюсы: устойчив к GPU-атакам. Минусы: требует ресурсов, может быть недоступен на старых версиях PHP.
Как оценить надёжность создаваемого пароля?
Регулярные выражения или библиотеки типа zxcvbn помогают оценить сложность:
$password = 'MyPassword123!';
if (preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/', $password)) {
echo 'Надёжный пароль';
}Ложные срабатывания, пользователи могут создавать слабые пароли, соответствующие шаблону.
Как сгенерировать надёжный пароль для пользователя?
Генерация выполняется с помощью random_bytes и кодировки в base64 или hex:
$bytes = random_bytes(16);
$password = bin2hex($bytes); // или base64_encodeСлишком сложные пароли трудно запомнить, нужно предлагать пользователю изменить.
// Регистрация
$password = 'User@123';
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
echo "Хэш для хранения: $hash\n";
// Аутентификация
$input = 'User@123';
if (password_verify($input, $hash)) {
echo "Пароль верен\n";
// проверка необходимости обновления
if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) {
$newHash = password_hash($input, PASSWORD_BCRYPT, ['cost' => 12]);
echo "Хэш обновлён: $newHash\n";
}
} else {
echo "Неверный пароль\n";
}Хэш для хранения: $2y$10$... Пароль верен Хэш обновлён: $2y$12$...
Пояснение: При входе проверяется пароль, затем вызывается password_needs_rehash для проверки необходимости обновления алгоритма или стоимости. Если требуется, создаётся новый хэш и сохраняется в базу.
$password = 'test123';
$md5Hash = md5($password);
$bcryptHash = password_hash($password, PASSWORD_BCRYPT);
echo "MD5 хэш: $md5Hash\n";
echo "Bcrypt хэш: $bcryptHash\n";MD5 хэш: cc03e747a6afbbcbf8be7668acfebee5 Bcrypt хэш: $2y$10$... (длинная строка)
Пояснение: MD5 вычисляется за микросекунды, bcrypt требует около 0.1 секунды за счёт стоимости. Это делает атаку перебором в миллионы раз дороже.
if (defined('PASSWORD_ARGON2ID')) {
$options = ['memory_cost' => 1<<17, 'time_cost' => 4, 'threads' => 4];
$hash = password_hash($password, PASSWORD_ARGON2ID, $options);
echo "Argon2 хэш: $hash\n";
} else {
echo "Argon2 недоступен\n";
}Argon2 хэш: $argon2id$v=19$m=131072,t=4,p=4$... (длинная строка)
Пояснение: Argon2id использует память и время, что затрудняет атаки на GPU. Настройки memory_cost, time_cost, threads нужно адаптировать под сервер.
// Генерация пароля
$bytes = random_bytes(12);
$generatedPassword = bin2hex($bytes); // 24 символа
echo "Сгенерированный пароль: $generatedPassword\n";
// Проверка сложности
$pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{12,}$/';
if (preg_match($pattern, $generatedPassword)) {
echo "Пароль соответствует требованиям\n";
} else {
echo "Пароль не проходит проверку (маловероятно)\n";
}Сгенерированный пароль: a1b2c3d4e5f6g7h8i9j0k1l2 Пароль соответствует требованиям
Пояснение: random_bytes даёт случайные байты, bin2hex преобразует в hex-строку, которая содержит только 0-9a-f. Для увеличения разнообразия лучше использовать base64 или другие энкодеры, но для примера достаточно. Проверка сложности может не пройти из-за отсутствия спецсимволов.
$salt = '$2y$10$' . substr(str_replace('+', '.', base64_encode(random_bytes(22))), 0, 22);
$hash = crypt($password, $salt);
echo "Хэш крипта: $hash\n";Хэш крипта: $2y$10$... (длинная строка)
Пояснение: crypt требует строго 22 символа для соли после префикса. Используется base64 с заменой '+' на '.' для соответствия алфавиту. Этот способ устарел, проще использовать password_hash.