Сценарии проверки личности в веб-безопасности PHP
Аутентификация в PHP: подходы и реализация
Современная сессионная аутентификация с хешированием паролей (bcrypt)
Этот метод считается наиболее безопасным для веб-приложений, где требуется управление сессиями на сервере. Пароли хранятся в виде хеша с солью (алгоритм bcrypt). Сессия создается после успешной проверки логина и пароля, а идентификатор сессии хранится в cookie с флагами HttpOnly, Secure, SameSite.
Как реализовать регистрацию и вход с защитой от распространенных уязвимостей?
Регистрация: при получении пароля используем password_hash($password, PASSWORD_BCRYPT). Храним в БД логин и хеш. Вход: извлекаем хеш по логину, проверяем password_verify($password, $hash). При успехе - регенерируем ID сессии (session_regenerate_id(true)) и сохраняем данные пользователя в $_SESSION.
// registration.php
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_BCRYPT);
// сохраняем $hash в БД вместе с логином
// login.php
$user = getUserByLogin($_POST['login']);
if ($user && password_verify($_POST['password'], $user['hash'])) {
session_start();
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];
// редирект
}
Access php (доступ к файлам в php)
Типичные ошибки: не использовать password_hash с устаревшими алгоритмами (MD5, SHA1); не проверять длину и сложность пароля; не обновлять сессионный ID после входа; не устанавливать параметры cookie (HttpOnly, Secure). Решение: всегда использовать PASSWORD_BCRYPT или PASSWORD_ARGON2I; проверять пароль на минимальную длину; вызывать session_regenerate_id() при каждом успешном входе; настраивать cookie через session_set_cookie_params(...).
Этот подход подходит для большинства веб-сайтов, где требуется постоянная аутентификация пользователей.
Как организовать аутентификацию без сессий, используя только JWT (JSON Web Token)?
JWT позволяет хранить утверждения (claims) в самом токене, подписанном секретным ключом. Сервер не хранит состояние сессии. Токен передается в HTTP-заголовке Authorization: Bearer. Подходит для API и микросервисов. Реализация: при входе создаем JWT с payload (user_id, role, exp), подписываем с помощью HMAC-SHA256 (или RS256). Клиент хранит токен (в localStorage, но безопаснее в HttpOnly cookie). Каждый запрос проверяет подпись и срок действия.
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$key = 'secret';
$payload = [
'iss' => 'your-site',
'iat' => time(),
'exp' => time() + 3600,
'user_id' => $user['id'],
'role' => $user['role']
];
$jwt = JWT::encode($payload, $key, 'HS256');
// отправляем клиенту
// проверка
try {
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
// доступ к $decoded->user_id
} catch (\Exception $e) {
// ошибка валидации
}
Php filter (фильтрация данных в php)
Проблемы: токен не может быть отозван до истечения срока (если не использовать черный список); утечка токена дает доступ злоумышленнику; сложность с обновлением токена (refresh token). Решение: использовать короткоживущие access token (15–30 мин) и долгоживущие refresh token с хранением в БД; хранить refresh token в HttpOnly cookie.
Как просто защитить страницу с помощью базовой аутентификации HTTP?
Самый простой способ для административных разделов или API. Клиент отправляет заголовок Authorization: Basic base64(login:password). Сервер проверяет. Не рекомендуется для пользовательских сайтов из-за передачи пароля в каждом запросе (даже если через HTTPS).
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Restricted Area"');
header('HTTP/1.0 401 Unauthorized');
echo 'Access denied';
exit;
}
$login = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
// проверка
Php пароль (работа с паролями в php)
Пароль передается в открытом виде (base64 - не шифрование). Только HTTPS. Нет механизма выхода (logout), кроме закрытия браузера. Решение: использовать только для внутренних инструментов, обязательно через HTTPS.
Как добавить дополнительный фактор на основе временных одноразовых паролей (Google Authenticator)?
После успешного ввода логина/пароля пользователь вводит код из приложения-аутентификатора. На сервере генерируется секретный ключ (base32), который сканируется пользователем. Проверка: вычисляем TOTP по RFC 6238.
// Генерация секрета (например, через Sonata\GoogleAuthenticator)
$g = new GoogleAuthenticator();
$secret = $g->generateSecret();
// сохранить $secret для пользователя
// Проверка кода
$isValid = $g->checkCode($secret, $_POST['code']);
Tokens php (токены в php)
Проблемы: синхронизация времени, потеря доступа к приложению (backup codes). Решение: предоставить резервные коды при настройке 2FA.
Как разрешить вход через Google, GitHub с помощью OAuth2?
Пользователь перенаправляется на страницу провайдера, после авторизации получает authorization code, который обменивается на access token. Затем запрашиваются данные пользователя. Упрощает регистрацию, но требует доверия внешнему сервису.
// Пример с использованием библиотеки league/oauth2-client
$provider = new \League\OAuth2\Client\Provider\Google([
'clientId' => '...',
'clientSecret' => '...',
'redirectUri' => '...',
]);
if (!isset($_GET['code'])) {
$authUrl = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
header('Location: ' . $authUrl);
exit;
} else {
$token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
$user = $provider->getResourceOwner($token);
// логин по email
}
Проблемы: необходимость регистрации приложения у провайдера; безопасность callback URL; возможные redirect с подделкой state. Решение: проверять state-параметр для защиты от CSRF; хранить state в сессии.
Подробные примеры реализации
Пример 1. Сессионная аутентификация с защитой от CSRF и блокировкой после неудачных попыток
Код регистрации и входа с использованием встроенных средств PHP для CSRF токенов. Покажем листинг с обработкой попыток.
<?php
// config.php
session_start();
define('MAX_ATTEMPTS', 5);
define('BLOCK_TIME', 900); // 15 минут
// registration.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$login = trim($_POST['login']);
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// save to DB: login, hash, created_at
// generate CSRF token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// login.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// check CSRF
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token mismatch');
}
// check block
$attempts = $_SESSION['attempts'] ?? 0;
$block_time = $_SESSION['block_time'] ?? 0;
if ($attempts >= MAX_ATTEMPTS && time() - $block_time < BLOCK_TIME) {
die('Too many attempts. Try later.');
}
$user = getUserByLogin($_POST['login']);
if ($user && password_verify($_POST['password'], $user['hash'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
unset($_SESSION['attempts'], $_SESSION['block_time']);
// regenerate CSRF
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
header('Location: dashboard.php');
exit;
} else {
$_SESSION['attempts'] = ($_SESSION['attempts'] ?? 0) + 1;
if ($_SESSION['attempts'] >= MAX_ATTEMPTS) {
$_SESSION['block_time'] = time();
}
$error = 'Invalid credentials';
}
}
?>
Результат: пользователь блокируется на 15 минут после 5 неудачных попыток. CSRF токен защищает форму от подделки.
Пример 2. JWT аутентификация с refresh token
Создаем два токена: access (15 мин) и refresh (7 дней). Refresh хранится в БД и в HttpOnly cookie. При истечении access, клиент отправляет refresh на специальный endpoint.
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$accessKey = 'access_secret';
$refreshKey = 'refresh_secret';
$accessExp = time() + 900;
$refreshExp = time() + 604800;
// Создание токенов при входе
$accessPayload = ['user_id' => $user['id'], 'type' => 'access', 'exp' => $accessExp];
$accessToken = JWT::encode($accessPayload, $accessKey, 'HS256');
$refreshPayload = ['user_id' => $user['id'], 'type' => 'refresh', 'exp' => $refreshExp];
$refreshToken = JWT::encode($refreshPayload, $refreshKey, 'HS256');
// Сохраняем refresh в БД (таблица refresh_tokens: user_id, token_hash, expires_at)
$tokenHash = hash('sha256', $refreshToken);
// INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($user['id'], '$tokenHash', date('Y-m-d H:i:s', $refreshExp))
// Отправляем refresh token в HttpOnly cookie (недоступен JS)
setcookie('refresh_token', $refreshToken, $refreshExp, '/', '', true, true);
// Access token передаем клиенту в ответе JSON
echo json_encode(['access_token' => $accessToken]);
// Endpoint для обновления
// if cookie refresh_token exists
$refreshToken = $_COOKIE['refresh_token'] ?? '';
try {
$decoded = JWT::decode($refreshToken, new Key($refreshKey, 'HS256'));
if ($decoded->type !== 'refresh') throw new Exception;
// Проверяем, что такой токен есть в БД и не отозван
$stored = getRefreshTokenByHash(hash('sha256', $refreshToken));
if (!$stored || $stored['revoked']) throw new Exception;
// Создаем новый access token
$newAccess = JWT::encode(['user_id' => $decoded->user_id, 'type' => 'access', 'exp' => time()+900], $accessKey, 'HS256');
echo json_encode(['access_token' => $newAccess]);
} catch (Exception $e) {
http_response_code(401);
echo 'Invalid refresh token';
}
?>
Результат: клиент получает короткоживущий access token, а refresh token хранится в HttpOnly cookie. При запросе защищенного ресурса сервер проверяет access. Если истек, клиент автоматически (через interceptor) запрашивает новый access по refresh.
Пример 3. Двухфакторная аутентификация с TOTP (Google Authenticator)
Реализация с использованием библиотеки (например, Spomky-Labs/otphp).
<?php
use OTPHP\TOTP;
// Настройка 2FA (после обычного входа)
$secret = TOTP::create()->getSecret(); // base32
// Показываем QR-код: otpauth://totp/Example:user?secret=...&issuer=Example
// Сохраняем secret у пользователя
// Проверка кода
$totp = TOTP::create($secret);
$isValid = $totp->verify($_POST['code'], null, 2); // допускаем 2 шага по времени
if ($isValid) {
// завершаем аутентификацию (например, устанавливаем сессию)
$_SESSION['2fa_verified'] = true;
}
?>
Результат: пользователь после ввода пароля должен ввести 6-значный код, который генерируется каждые 30 секунд в приложении. Без знания секрета подобрать код практически невозможно.