Реализация администраторской авторизации в PHP с примерами
Основные подходы к авторизации администратора в PHP
Сессионная авторизация с проверкой роли (наиболее эффективное решение)
Этот подход подразумевает хранение идентификатора пользователя и его роли (например, 'admin') в сессии PHP после успешной аутентификации. Авторизация (проверка прав) выполняется на каждой странице, требующей административного доступа, путём чтения данных из сессии.
Основные шаги реализации:
- Создание формы входа (логин, пароль).
- Валидация данных на стороне сервера.
- Проверка учётных данных (против хэша пароля, сохранённого в базе данных).
- Запуск сессии, сохранение идентификатора администратора и роли.
- Регенерация ID сессии после успешного входа для предотвращения фиксации сессии.
- Разграничение доступа к страницам на основе роли.
<?php
session_start();
require 'db.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$password = $_POST['password'] ?? '';
if (!$email || !$password) {
$error = 'Пожалуйста, заполните все поля.';
} else {
$stmt = $pdo->prepare('SELECT id, password, role FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password']) && $user['role'] === 'admin') {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];
header('Location: /admin/dashboard.php');
exit;
} else {
$error = 'Неверный логин, пароль или недостаточно прав.';
}
}
}
?>
После входа в каждой странице админки добавляется проверка:
<?php
session_start();
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
header('HTTP/1.0 403 Forbidden');
die('Доступ запрещён.');
}
?>
Типичные проблемы и их решения:
- Фиксация сессии: решается регенерацией ID сессии при входе (session_regenerate_id).
- XSS-атаки: экранирование вывода (htmlspecialchars) и установка флага HttpOnly для cookie сессии.
- CSRF: использование токенов для критических операций (например, смена пароля).
- SQL-инъекции: использование подготовленных запросов PDO или mysqli.
- Слабый пароль: хэширование bcrypt (password_hash / password_verify).
Как организовать авторизацию администратора без сохранения состояния на сервере?
Использование JWT (JSON Web Token). Сервер выдаёт токен после аутентификации, клиент отправляет его с каждым запросом. Сервер проверяет подпись токена и извлекает роль администратора из полезной нагрузки.
<?php
// Генерация JWT (используется библиотека firebase/php-jwt)
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
$key = 'секретный_ключ';
$payload = [
'user_id' => $user['id'],
'role' => $user['role'],
'iat' => time(),
'exp' => time() + 3600
];
$jwt = JWT::encode($payload, $key, 'HS256');
setcookie('admin_token', $jwt, time()+3600, '/', '', true, true); // Secure + HttpOnly
?>
<?php
// Проверка JWT на защищённой странице
$jwt = $_COOKIE['admin_token'] ?? '';
try {
$decoded = JWT::decode($jwt, new \Firebase\JWT\Key($key, 'HS256'));
if ($decoded->role !== 'admin') throw new Exception('Не админ');
// Доступ разрешён
} catch (Exception $e) {
header('HTTP/1.0 401 Unauthorized');
echo 'Токен недействителен.';
exit;
}
?>
Возможные проблемы:
- Токен может быть украден при XSS, если хранится в localStorage. Куки с флагами Secure и HttpOnly снижают риск.
- Отзыв токена до истечения срока требует чёрного списка (сложнее).
- Синхронизация ключа подписи при масштабировании.
Как использовать встроенную базовую аутентификацию для администратора?
Механизм HTTP Basic Auth прост, но мало подходит для веб-приложений без HTTPS из-за передачи пароля в открытом виде.
<?php
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Admin Zone"');
header('HTTP/1.0 401 Unauthorized');
echo 'Отменено пользователем';
exit;
} else {
$user = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
// Проверка против БД (аналогично сессии)
if (checkAdmin($user, $pass)) {
// Доступ разрешён
} else {
header('HTTP/1.0 401 Unauthorized');
echo 'Неверные учётные данные';
exit;
}
}
?>
Проблемы:
- Нет встроенного механизма выхода (браузер хранит учётные данные до закрытия).
- Пароль передаётся в base64 без шифрования (требуется HTTPS).
- Неудобно для реализации разграничения ролей (всегда только одно имя пользователя).
Как внедрить многофакторную авторизацию для администрирования?
После проверки пароля запрашивается одноразовый код (TOTP) через Google Authenticator. Реализация с помощью библиотеки PHPGangsta/GoogleAuthenticator.
<?php
// После успешной проверки пароля
$_SESSION['partial_auth'] = true; // флаг первого этапа
header('Location: /admin/verify-2fa.php');
?>
// verify-2fa.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$code = $_POST['code'];
$ga = new PHPGangsta_GoogleAuthenticator();
$secret = 'секрет администратора'; // из БД
if ($ga->verifyCode($secret, $code, 2)) {
$_SESSION['role'] = 'admin';
unset($_SESSION['partial_auth']);
header('Location: /admin/dashboard.php');
} else {
$error = 'Неверный код';
}
}
?>
Проблемы:
- Необходимость хранить секретные ключи для каждого администратора.
- Пользователь может потерять устройство с TOTP.
- Дополнительная нагрузка на UX.
Расширенные примеры кода для авторизации администратора
Ниже приведены подробные примеры, которые демонстрируют не только базовую авторизацию, но и дополнительные меры безопасности.
Пример 1: Полный скрипт входа с защитой от перебора
<?php
session_start();
require 'config.php';
$max_attempts = 5;
$lockout_time = 900; // 15 минут
$ip = $_SERVER['REMOTE_ADDR'];
$attempt_key = 'login_attempts_'.$ip;
$lock_key = 'login_lock_'.$ip;
// Проверка блокировки
if (isset($_SESSION[$lock_key]) && (time() - $_SESSION[$lock_key]) < $lockout_time) {
die('Слишком много попыток. Попробуйте через 15 минут.');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$password = $_POST['password'] ?? '';
if (!$email || !$password) {
$error = 'Заполните поля.';
} else {
// Проверка учётных данных
$stmt = $pdo->prepare('SELECT id, password, role, name FROM admin_users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
if ($user['role'] === 'admin') {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];
$_SESSION['name'] = $user['name'];
// Сброс счётчика попыток
unset($_SESSION[$attempt_key], $_SESSION[$lock_key]);
header('Location: dashboard.php');
exit;
} else {
$error = 'Недостаточно прав (роль не admin).';
}
} else {
// Увеличиваем счётчик неудачных попыток
$_SESSION[$attempt_key] = ($_SESSION[$attempt_key] ?? 0) + 1;
if ($_SESSION[$attempt_key] >= $max_attempts) {
$_SESSION[$lock_key] = time();
$error = 'Аккаунт заблокирован из-за множества неудачных попыток.';
} else {
$error = 'Неверный email или пароль.';
}
}
}
}
?>
(При первой неудачной попытке пользователь видит «Неверный email или пароль.»; после 5 ошибок появляется блокировка на 15 минут.)
Пример 2: Middleware для проверки прав администратора (диспетчерский паттерн)
<?php
// admin_check.php
session_start();
function isAdmin(): bool {
return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
}
function requireAdmin(): void {
if (!isAdmin()) {
header('HTTP/1.0 403 Forbidden');
include '403.php';
exit;
}
}
// Использование на защищённой странице
require_once 'admin_check.php';
requireAdmin();
// Остальной код страницы
?>
(Страница выводит кастомную страницу 403, если пользователь не является администратором.)
Пример 3: Авторизация через JWT с обновлением токена (refresh token)
<?php
// login.php - выдача access и refresh токенов
use Firebase\JWT\JWT;
$access_key = 'access_secret';
$refresh_key = 'refresh_secret';
// После успешной аутентификации
$access_payload = [
'sub' => $user['id'],
'role' => 'admin',
'iat' => time(),
'exp' => time() + 900 // 15 минут
];
$refresh_payload = [
'sub' => $user['id'],
'token_hash' => bin2hex(random_bytes(16)),
'iat' => time(),
'exp' => time() + 86400 * 7 // 7 дней
];
$access_token = JWT::encode($access_payload, $access_key, 'HS256');
$refresh_token = JWT::encode($refresh_payload, $refresh_key, 'HS256');
// Сохраняем refresh в БД вместе с токеном хэша для отзыва
setcookie('access_token', $access_token, time()+900, '/', '', true, true);
setcookie('refresh_token', $refresh_token, time()+86400*7, '/', '', true, true);
?>
// refresh.php - обновление access токена
$refresh = $_COOKIE['refresh_token'] ?? '';
try {
$decoded = JWT::decode($refresh, new \Firebase\JWT\Key($refresh_key, 'HS256'));
// Проверка существования refresh токена в БД
$stmt = $pdo->prepare('SELECT id, role FROM admin_users WHERE id = ?');
$stmt->execute([$decoded->sub]);
$user = $stmt->fetch();
if (!$user) throw new Exception('Пользователь не найден');
$new_access = JWT::encode([
'sub' => $user['id'],
'role' => $user['role'],
'iat' => time(),
'exp' => time() + 900
], $access_key, 'HS256');
setcookie('access_token', $new_access, time()+900, '/', '', true, true);
} catch (Exception $e) {
header('HTTP/1.0 401 Unauthorized');
echo 'Требуется повторный вход.';
}
?>
(Клиент автоматически обновляет access токен, когда тот истекает, используя refresh cookie. Это снижает риск компрометации долгоживущего токена.)
Пример 4: Иерархия ролей (superadmin, admin, moderator)
<?php
// RBAC простейшая реализация с конфигурацией
$roles = [
'superadmin' => ['permissions' => ['manage_admins', 'delete_content', 'edit_settings']],
'admin' => ['permissions' => ['delete_content', 'edit_settings']],
'moderator' => ['permissions' => ['edit_content']],
];
function hasPermission(string $permission, string $role): bool {
global $roles;
return in_array($permission, $roles[$role]['permissions'] ?? []);
}
// Проверка на странице удаления контента
if (!hasPermission('delete_content', $_SESSION['role'])) {
die('Недостаточно прав для удаления.');
}
?>
(Администраторы с ролью admin могут удалять контент, а модераторы - только редактировать. Superadmin может управлять другими администраторами.)