Как защитить PHP-приложение: полный гайд по безопасной аутентификации

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему «своя велосипедная» аутентификация — это мина замедленного действия

Каждый второй PHP-разработчик хотя бы раз писал собственную систему входа. Это кажется простым: форма, проверка логина, сессия. Но именно здесь, на этом «простом» этапе, гибнут миллионы сайтов. SQL-инъекции, кража сессий, подбор паролей — атаки, которые превращают ваш проект в раздачу личных данных.

В этой статье мы разберем безопасную аутентификацию в PHP с нуля. Никакой магии — только проверенные практики, которые используют в Facebook, GitHub и вашем любимом фреймворке. Вы узнаете, как правильно хешировать пароли, защищать сессии от перехвата и внедрять современные механизмы вроде JWT.

Готовы? Поехали.

1. Хеширование паролей: почему md5() — это смертный приговор

Первое правило безопасности: никогда, никогда не храните пароли в открытом виде. Второе правило: не используйте устаревшие алгоритмы вроде MD5 или SHA1. Они рассчитываются за микросекунды, и любой школьник с ноутбуком подберет ваш пароль за час.

В PHP есть встроенное решение — функции password_hash() и password_verify(). Они используют bcrypt (или Argon2, если PHP ≥ 7.2) и автоматически добавляют «соль» — случайную строку, которая делает каждый хеш уникальным.

Пример правильного хеширования

// Регистрация пользователя
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

// Сохраняем $hash в базу данных
// Никакой соли вручную! PHP сделает всё сам.

Проверка пароля при входе

// Логин пользователя
$inputPassword = $_POST['password'];
$storedHash = 'из базы данных';

if (password_verify($inputPassword, $storedHash)) {
    // Пароль верный — пускаем
} else {
    // Пароль неверный
}

Обратите внимание на параметр cost. Он задает «стоимость» вычисления хеша. Чем выше число, тем дольше злоумышленник будет подбирать пароль. Для 2024 года cost = 12 — золотая середина между безопасностью и скоростью.

2. Сессии: как не отдать ключи от дома

После успешного входа мы обычно стартуем сессию. Но стандартные PHP-сессии имеют слабые места: предсказуемые ID, утечка через URL, фиксация сессии (session fixation).

Безопасная настройка сессии

// В самом начале скрипта, до вывода любого HTML
ini_set('session.use_only_cookies', 1);
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // Только для HTTPS
ini_set('session.cookie_samesite', 'Lax');

session_start();

// Регенерируем ID сессии после входа
if ($login_success) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
}

Что мы сделали:

  • use_only_cookies — запрещаем передачу ID сессии через URL (чтобы его не украли из ссылки)
  • use_strict_mode — блокируем неинициализированные сессии
  • httponly — запрещаем JavaScript доступ к куке сессии (защита от XSS)
  • secure — кука передается только по HTTPS
  • samesite — защита от CSRF-атак через подделку межсайтовых запросов

И главное: всегда регенерируйте ID сессии после входа. Иначе злоумышленник может заранее подсунуть пользователю свой session ID и перехватить аккаунт.

3. Защита от CSRF: проверка, что запрос отправлен вами

CSRF (Cross-Site Request Forgery) — атака, когда пользователь переходит по ссылке вида /delete-account?id=1 и, сам того не желая, выполняет действие. Чтобы этого избежать, используйте токены.

Генерация и проверка CSRF-токена

// Генерация токена (в начале сессии или при загрузке формы)
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// В HTML-форме
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';

// При обработке формы
if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
    // Токен верный — выполняем действие
} else {
    // Подозрительный запрос — блокируем
}

Функция hash_equals() — важный момент. Она сравнивает строки за константное время, не давая атакующему понять, где именно ошибка (защита от timing attack).

4. JWT (JSON Web Tokens): когда сессии не подходят

Если вы пишете API или SPA (React, Vue), классические сессии неудобны. На помощь приходят JWT — токены, которые содержат все данные о пользователе и подписаны секретным ключом.

Но будьте осторожны: JWT — не панацея. Если токен украдут, его можно использовать до истечения срока. Поэтому:

  • Устанавливайте короткое время жизни (15–30 минут)
  • Используйте refresh-токены для обновления
  • Храните JWT в HttpOnly cookie, а не в localStorage

Пример генерации JWT (с использованием библиотеки firebase/php-jwt)

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$secretKey = 'ваш-секретный-ключ-длиной-не-менее-32-символа';
$payload = [
    'user_id' => 123,
    'email' => 'user@example.com',
    'iat' => time(),
    'exp' => time() + 900 // 15 минут
];

$jwt = JWT::encode($payload, $secretKey, 'HS256');

// Отправляем токен в cookie
setcookie('auth_token', $jwt, [
    'expires' => time() + 900,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict'
]);

Проверка JWT при каждом запросе

try {
    $decoded = JWT::decode($_COOKIE['auth_token'], new Key($secretKey, 'HS256'));
    // $decoded->user_id — ваш пользователь
} catch (\Exception $e) {
    // Токен недействителен или истек
    http_response_code(401);
    exit;
}

5. Дополнительные меры: лишними не будут

Лимит попыток входа (Brute Force Protection

Блоги

Рекомендаций по книгам