Avatar ·

Безопасное хеширование и соль для паролей в PHP: как правильно?

Всем привет. Я начинающий PHP-разработчик, и у меня возникла проблема с хранением паролей пользователей. Сейчас я использую простой md5(), но прочитал, что это небезопасно. Попробовал добавить соль, но не уверен, что делаю это правильно. Вот мой текущий код для регистрации: ```php $password = $_POST['password']; $salt = 'my_secret_salt'; $hashed_password = md5($salt . $password); // сохраняю $hashed_password в БД ``` А для проверки при логине: ```php $password = $_POST['password']; $salt = 'my_secret_salt'; $hashed_password = md5($salt . $password); // сравниваю с тем, что в БД ``` Проблемы, которые я вижу: 1. Соль статическая — если её узнают, все пароли скомпрометированы. 2. md5() слишком быстрый, его легко подобрать. 3. Нет защиты от атак по времени (timing attack). Подскажите, как правильно реализовать хеширование паролей в PHP? Какие функции использовать? Нужно ли генерировать уникальную соль для каждого пользователя и где её хранить? Буду благодарен за примеры кода и объяснение best practices. Версия PHP 7.4, использую MySQL. <<>>

Правильное хеширование паролей в PHP

Вы абсолютно правы: md5() и статическая соль — это устаревший и опасный подход. Современный PHP предоставляет встроенные функции для безопасного хеширования, которые решают все ваши проблемы. Вот правильное решение.

Используйте password_hash() и password_verify()

Начиная с PHP 5.5, существуют две ключевые функции:

  • password_hash() — создаёт хеш пароля с автоматической генерацией соли и использованием алгоритма bcrypt (по умолчанию).
  • password_verify() — проверяет пароль против хеша.

Пример регистрации пользователя

<?php
// Получаем пароль из формы
$password = $_POST['password'];

// Хешируем пароль (соль генерируется автоматически)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);

// Сохраняем $hashed_password в БД (например, в поле VARCHAR(255))
$sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST['username'], $hashed_password]);
?>

Пример проверки пароля при логине

<?php
// Получаем пароль из формы
$password = $_POST['password'];

// Достаём хеш из БД по имени пользователя
$sql = "SELECT password_hash FROM users WHERE username = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST['username']]);
$user = $stmt->fetch();

if ($user && password_verify($password, $user['password_hash'])) {
    echo "Успешный вход!";
} else {
    echo "Неверный логин или пароль";
}
?>

Почему это безопасно?

  • Автоматическая соль: Каждый хеш содержит уникальную соль, сгенерированную криптографически стойким генератором. Соль хранится прямо в строке хеша — вам не нужно отдельное поле в БД.
  • bcrypt: Алгоритм медленный (намеренно), что делает атаки перебором практически невозможными. Одна итерация bcrypt занимает ~100-200 мс.
  • Защита от timing-атак: password_verify() использует константное время сравнения.
  • Будущая совместимость: PASSWORD_DEFAULT автоматически обновит алгоритм при выходе новых версий PHP.

Дополнительные настройки (опционально)

Вы можете увеличить стоимость (cost factor) для bcrypt, чтобы сделать хеширование ещё медленнее:

$options = ['cost' => 12]; // По умолчанию 10, можно 12-14 для большей безопасности
$hashed_password = password_hash($password, PASSWORD_BCRYPT, $options);

Важно: не ставьте слишком высокий cost — проверьте на своём сервере, чтобы время хеширования не превышало 0.5 секунды.

Что хранить в БД?

Вам нужно только одно поле VARCHAR(255) (bcrypt даёт строку длиной 60 символов, но лучше взять с запасом). Соль и алгоритм уже встроены в эту строку. Пример хеша:

$2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

Здесь $2y$10$ — это идентификатор алгоритма и стоимость, а остальное — соль (22 символа) и хеш.

Итог

Забудьте про md5(), sha1() и ручную соль. Используйте только password_hash() и password_verify() — это стандарт безопасности в PHP. Ваш код станет проще, а пароли — защищёнными.

<<>>

Дополнительные меры безопасности

Коллега верно описал основной подход. Я добавлю несколько важных моментов, которые помогут ещё больше усилить защиту.

1. Проверка на утечки (Have I Been Pwned)

Перед хешированием полезно проверить, не был ли пароль скомпрометирован. Можно использовать API от Have I Been Pwned (k-anonymity):

<?php
function is_password_pwned(string $password): bool {
    $hash = strtoupper(sha1($password));
    $prefix = substr($hash, 0, 5);
    $suffix = substr($hash, 5);
    $response = file_get_contents("https://api.pwnedpasswords.com/range/{$prefix}");
    return strpos($response, $suffix) !== false;
}

if (is_password_pwned($password)) {
    echo "Этот пароль был скомпрометирован, выберите другой.";
}
?>

2. Ограничение попыток входа

Даже с bcrypt стоит защищаться от брутфорса. Реализуйте задержку после нескольких неудачных попыток:

  • Храните в БД количество неудачных попыток и время последней.
  • После 5 ошибок блокируйте вход на 15 минут.
  • Используйте CAPTCHA после 3 неудач.

3. Двухфакторная аутентификация (2FA)

Для критичных приложений добавьте 2FA через TOTP (Google Authenticator). Библиотеки: spomky-labs/otphp или sonata-project/google-authenticator.

4. Обновление хешей

Если в будущем вы измените алгоритм или стоимость, можно автоматически обновлять хеши при входе пользователя:

if (password_verify($password, $user['hash'])) {
    if (password_needs_rehash($user['hash'], PASSWORD_DEFAULT, ['cost' => 12])) {
        $newHash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
        // обновить в БД
    }
}

Помните: безопасность — это многослойная защита. Хеширование — только первый слой, но крайне важный. Удачи в разработке!