Почему «своя велосипедная» аутентификация — это мина замедленного действия
Каждый второй 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 в
HttpOnlycookie, а не в 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;
}