Полное руководство по системе логина и работе с пользователями
Основные подходы к аутентификации и управлению пользователями
Эффективное решение на основе сессий и prepared statements
Наиболее распространённый и безопасный способ авторизации в классических веб-приложениях - использование серверных сессий. Пароли хранятся в хэшированном виде (password_hash), а идентификатор пользователя сохраняется в сессии после успешного входа. Для управления пользователями создаётся таблица users с полями id, email, password_hash, role, и т.д.
Пример минимального файла index.php?com=login:
// index.php
$action = $_GET['com'] ?? '';
if ($action === 'login') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'];
$password = $_POST['password'];
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$stmt = $db->prepare('SELECT id, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
session_start();
$_SESSION['user_id'] = $user['id'];
session_regenerate_id();
header('Location: dashboard.php');
exit;
}
$error = 'Неверные учетные данные';
}
// форма логина
}
После входа на защищённых страницах проверяется наличие $_SESSION['user_id']. Для выхода вызывается session_destroy().
Типичные ошибки:
- Использование
sha1илиmd5вместоpassword_hash- пароли легко восстанавливаются. - Отсутствие
session_regenerate_id()- уязвимость к фиксации сессии. - Прямая конкатенация в SQL - инъекции. Решение: только prepared statements.
Как организовать «Запомнить меня» через куки?
Для долгосрочного хранения сессии без повторного ввода пароля используется кука с токеном. При входе генерируется случайный токен, сохраняется в базе и в куку. При последующем визите по куке восстанавливается сессия.
// при успешном логине
if ($remember) {
$token = bin2hex(random_bytes(32));
$stmt = $db->prepare('INSERT INTO auth_tokens (user_id, token) VALUES (?, ?)');
$stmt->execute([$user['id'], hash('sha256', $token)]);
setcookie('remember', $token, time() + 86400 * 30, '/', '', true, true);
}
Ошибка: хранение токена в куке без хэша - компрометация куки даёт доступ. Решение: хранить хэш в базе.
Как реализовать JWT-авторизацию для API?
JWT (JSON Web Token) позволяет избежать хранения сессии на сервере. Токен подписывается секретом и содержит данные пользователя. Подходит для REST API и микросервисов.
use Firebase\JWT\JWT;
$key = 'secret';
$payload = [
'user_id' => 123,
'role' => 'admin',
'exp' => time() + 3600
];
$jwt = JWT::encode($payload, $key, 'HS256');
// отправляем клиенту
Проверка: декодируем JWT, извлекаем user_id, доступ к данным через middleware.
Ошибка: использование слишком короткого или публичного секрета. Решение: генерировать сложный ключ, хранить вне кода.
Как добавить вход через социальные сети (OAuth2)?
OAuth2 делегирует аутентификацию провайдеру (Google, GitHub). Пользователь перенаправляется на страницу провайдера, после подтверждения получает код авторизации, обменивает его на токен доступа и затем получает профиль.
// пример с библиотекой league/oauth2-client
$provider = new Google(['clientId' => '...', 'clientSecret' => '...', 'redirectUri' => '...']);
if (!isset($_GET['code'])) {
$authUrl = $provider->getAuthorizationUrl();
header('Location: '.$authUrl);
} else {
$token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
$user = $provider->getResourceOwner($token);
// логин/регистрация по email
}
Ошибка: незащищённый redirect_uri - редирект на опасный сайт. Решение: проверять совпадение.
Как настроить базовую HTTP-аутентификацию?
Простой метод для защищённых областей: клиент отправляет логин и пароль в заголовке Authorization: Basic .... Используется редко из-за отсутствия гибкости, но подходит для API.
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My Site"');
header('HTTP/1.0 401 Unauthorized');
exit;
} else {
$user = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
// проверка
}
Ошибка: передача данных в открытом виде без HTTPS. Решение: обязательное использование HTTPS.
Как внедрить двухфакторную аутентификацию (2FA)?
После успешного ввода пароля запрашивается одноразовый код из TOTP-приложения (Google Authenticator) или по SMS.
// генерация секрета
$secret = Google2FA::generateSecretKey();
// сохранение в БД
// при входе - проверка
$valid = Google2FA::verifyKey($secret, $_POST['otp']);
Ошибка: синхронизация времени. Решение: использовать библиотеки с учётом допустимой задержки.
Расширенные примеры с полным кодом и результатами
Регистрация с подтверждением email
После регистрации генерируется токен, отправляется письмо. При переходе по ссылке происходит активация.
// регистрация
$token = bin2hex(random_bytes(32));
$stmt = $db->prepare('INSERT INTO users (email, password_hash, confirmation_token) VALUES (?, ?, ?)');
$stmt->execute([$email, password_hash($pass, PASSWORD_DEFAULT), $token]);
$link = "https://example.com/confirm?token=$token";
// отправка письма (mail() или PHPMailer)
Результат: в почтовом ящике письмо со ссылкой вида https://example.com/confirm?token=3f8a2b...
Восстановление пароля с временным токеном
Алгоритм: запрос email -> создание токена с временем жизни -> отправка -> проверка -> смена пароля.
// запрос
$token = bin2hex(random_bytes(16));
$exp = date('Y-m-d H:i:s', strtotime('+1 hour'));
$stmt = $db->prepare('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)');
$stmt->execute([$email, hash('sha256', $token), $exp]);
// ссылка: https://example.com/reset?token=$token
Результат: письмо с одноразовой ссылкой, действительной 1 час.
Лимитирование попыток входа (rate limiting)
Для предотвращения брутфорса записываются IP и время попыток. При превышении лимита - временная блокировка.
$ip = $_SERVER['REMOTE_ADDR'];
$stmt = $db->prepare('SELECT COUNT(*) FROM login_attempts WHERE ip = ? AND attempted_at > NOW() - INTERVAL 15 MINUTE');
$stmt->execute([$ip]);
if ($stmt->fetchColumn() >= 5) {
exit('Слишком много попыток. Повторите через 15 минут.');
}
// после неудачной попытки
$stmt = $db->prepare('INSERT INTO login_attempts (ip, attempted_at) VALUES (?, NOW())');
$stmt->execute([$ip]);
Результат: после 5 неудачных попыток с одного IP в течение 15 минут вход блокируется.
Использование сессий в Redis для высокой нагрузки
Вместо файлов сессии можно хранить в Redis, что ускоряет работу и даёт возможность масштабирования.
// установка в php.ini или в коде
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://127.0.0.1:6379');
// работа с сессиями остаётся такой же
session_start();
$_SESSION['user_id'] = 123;
Результат: данные сессии хранятся в Redis, что повышает производительность на кластерных конфигурациях.
Middleware для проверки ролей (на примере Laravel-подобного роутинга)
Собственная реализация: массив маршрутов с required_role и функция проверки.
function middleware($requiredRole) {
session_start();
if (!isset($_SESSION['role']) || $_SESSION['role'] !== $requiredRole) {
header('HTTP/1.0 403 Forbidden');
exit('Доступ запрещён');
}
}
// в начале скрипта
if ($_GET['action'] === 'admin') {
middleware('admin');
// код для администратора
}
Результат: пользователь без роли admin получает 403 ошибку.