HTTP аутентификация скрипта index.php с помощью PHP и серверных директив
Основные принципы HTTP-аутентификации в PHP
Как защитить страницу index.php с помощью встроенной HTTP-аутентификации без использования сторонних библиотек?
Наиболее эффективное решение состоит в отправке заголовка WWW-Authenticate и проверке переменных окружения $_SERVER['PHP_AUTH_USER'] и $_SERVER['PHP_AUTH_PW']. Браузер при получении ответа 401 показывает диалоговое окно для ввода логина и пароля. После успешной отправки данных запрос повторяется с заголовком Authorization.
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Закрытая зона"');
header('HTTP/1.0 401 Unauthorized');
echo 'Необходима аутентификация';
exit;
} else {
$login = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
if ($login === 'admin' && $pass === 'secret') {
echo 'Доступ разрешен. Привет, ' . htmlspecialchars($login);
} else {
header('HTTP/1.0 401 Unauthorized');
echo 'Неверный логин или пароль';
}
}
Типичная ошибка: заголовки не работают, если до header() был вывод текста. Решение: убедиться, что никакой текст не выводится до отправки заголовков, или использовать буферизацию вывода (ob_start()).
Ещё одна проблема: при использовании CGI-версии PHP переменные PHP_AUTH_USER и PHP_AUTH_PW могут отсутствовать. В этом случае следует анализировать заголовок Authorization через $_SERVER['HTTP_AUTHORIZATION'] и декодировать Base64 строку:
if (!isset($_SERVER['PHP_AUTH_USER'])) {
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
}
}
Как реализовать дайджест-аутентификацию с использованием файла htdigest?
Дайджест-аутентификация более безопасна, так как пароль передается не в открытом виде, а в виде хеша. Для работы необходим файл, создаваемый утилитой htdigest (например, htdigest -c .htdigest "realm" user).
$realm = 'Закрытая зона';
$users_file = __DIR__ . '/.htdigest';
if (!isset($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Digest realm="' . $realm . '", qop="auth", nonce="' . uniqid() . '", opaque="' . md5($realm) . '"');
echo 'Необходима аутентификация';
exit;
} else {
// разбор строки $_SERVER['PHP_AUTH_DIGEST']
$digest = parseDigest($_SERVER['PHP_AUTH_DIGEST']);
$user_data = file($users_file, FILE_IGNORE_NEW_LINES);
$found = false;
foreach ($user_data as $line) {
list($u, $r, $hash) = explode(':', $line);
if ($u === $digest['username'] && $r === $realm) {
$A1 = $hash;
$A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $digest['uri']);
$valid_response = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $A2);
if ($digest['response'] === $valid_response) {
$found = true;
echo 'Доступ разрешен';
}
break;
}
}
if (!$found) {
header('HTTP/1.0 401 Unauthorized');
echo 'Доступ запрещен';
}
}
Проблемы: необходимость вручную разбирать строку дайджеста (функция parseDigest отсутствует в PHP, её нужно написать). Также браузер может не поддерживать Digest-метод в некоторых случаях. Рекомендуется проверять наличие PHP_AUTH_DIGEST и в случае отсутствия отправлять заголовок Basic как запасной вариант.
Как использовать .htaccess для HTTP-аутентификации с передачей управления PHP?
Если сервер Apache настроен на использование .htaccess, можно задать директивы аутентификации и обработать ошибку 401 через PHP. Пример в файле .htaccess:
AuthType Basic
AuthName "Private"
AuthUserFile /path/to/.htpasswd
Require valid-user
ErrorDocument 401 /index.php?error=401
В index.php можно проверить переменную $_GET['error'] и выполнить собственную логику, но при этом PHP уже получит данные аутентификации в переменных окружения (если не используется CGI).
Недостаток: пароли хранятся в файле .htpasswd, который должен быть недоступен для чтения из браузера. Также при использовании ErrorDocument заголовок 401 передается, но браузер может не показывать стандартное окно (в некоторых браузерах). Решение: не использовать ErrorDocument, а доверить аутентификацию полностью Apache.
Как объединить HTTP-аутентификацию с сессиями PHP для управления доступом?
После успешного прохождения базовой аутентификации можно стартовать сессию и сохранить статус авторизации. Это удобно для защиты нескольких страниц без повторного ввода пароля.
session_start();
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Private"');
header('HTTP/1.0 401 Unauthorized');
exit;
} else {
$login = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
// проверка пароля (например, через БД)
if (check_password($login, $pass)) {
$_SESSION['user'] = $login;
$_SESSION['auth_time'] = time();
echo 'Вы вошли как ' . $_SESSION['user'];
} else {
header('HTTP/1.0 401 Unauthorized');
echo 'Неверные данные';
}
}
Проблема: после закрытия браузера сессия может быть не уничтожена, но HTTP-аутентификация заставит снова запросить пароль. Если требуется явный выход (logout), можно очистить сессию и отправить заголовок с неверным realm, чтобы браузер забыл пароль. Однако стандартный механизм logout для Basic аутентификации не предусмотрен.
Как проверить HTTP-аутентификацию через базу данных MySQL?
При наличии базы пользователей пароль обычно хранится в виде хеша (например, bcrypt). Пример:
$dsn = 'mysql:host=localhost;dbname=users';
$pdo = new PDO($dsn, 'root', '');
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="DB Auth"');
header('HTTP/1.0 401 Unauthorized');
exit;
} else {
$stmt = $pdo->prepare('SELECT password FROM users WHERE login = ?');
$stmt->execute([$_SERVER['PHP_AUTH_USER']]);
$row = $stmt->fetch();
if ($row && password_verify($_SERVER['PHP_AUTH_PW'], $row['password'])) {
echo 'Доступ разрешен';
} else {
header('HTTP/1.0 401 Unauthorized');
echo 'Неверный логин или пароль';
}
}
Проблема: при каждом запросе (включая статические файлы) будет выполняться PHP-скрипт, если защищен весь каталог. Решение: использовать комбинацию с серверными директивами или токенами.
Расширенные примеры HTTP-аутентификации
Ниже представлены развёрнутые примеры с пошаговыми пояснениями и демонстрацией результатов.
Пример 1: Полный скрипт с выходом из учётной записи (logout) через URL параметр
В Basic-аутентификации трудно реализовать выход, так как браузер кеширует учётные данные. Метод: отправить заголовок с неверным realm, что заставит браузер перезапросить пароль. Однако true logout возможен только при использовании logout-токена.
// index.php
session_start();
$realm = 'Private Zone';
if (isset($_GET['logout'])) {
$_SESSION = [];
session_destroy();
header('WWW-Authenticate: Basic realm="' . $realm . '"');
header('HTTP/1.0 401 Unauthorized');
echo 'Вы вышли из системы';
exit;
}
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="' . $realm . '"');
header('HTTP/1.0 401 Unauthorized');
echo 'Для доступа необходима аутентификация';
exit;
} else {
$user = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
// Заглушка проверки
if ($user === 'demo' && $pass === '1234') {
$_SESSION['logged'] = true;
echo 'Привет, ' . htmlspecialchars($user) . '! <a href="?logout=1">Выход</a>';
} else {
header('HTTP/1.0 401 Unauthorized');
echo 'Неверные учётные данные';
}
}
Результат при правильном входе: браузер показывает страницу с приветствием и ссылкой на выход. При нажатии на ссылку параметр logout очищает сессию и снова отправляет заголовок 401, заставляя браузер запросить новые данные. Однако браузер может повторно отправить старые данные; для полного выхода требуется также очистить кеш паролей (что невозможно через PHP).
Пример 2: Аутентификация с поддержкой CGI и парсингом заголовка Authorization
Когда PHP работает через CGI (например, suPHP), переменные PHP_AUTH_USER не устанавливаются. Решение: перехватывать HTTP_AUTHORIZATION.
if (!isset($_SERVER['PHP_AUTH_USER'])) {
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$auth = $_SERVER['HTTP_AUTHORIZATION'];
if (strpos($auth, 'Basic ') === 0) {
$decoded = base64_decode(substr($auth, 6));
if ($decoded !== false && strpos($decoded, ':') !== false) {
list($user, $pass) = explode(':', $decoded, 2);
$_SERVER['PHP_AUTH_USER'] = $user;
$_SERVER['PHP_AUTH_PW'] = $pass;
}
}
}
}
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="CGI Safe"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
// Далее проверка $user и $pass
Результат: PHP корректно извлекает логин и пароль даже при работе через CGI. Без такого парсинга скрипт бы никогда не получил данные и постоянно отправлял 401.
Пример 3: Дайджест-аутентификация с полной обработкой (функция parseDigest)
function parseDigest($digest_str) {
$needed_parts = array('username'=>1, 'realm'=>1, 'nonce'=>1, 'uri'=>1, 'response'=>1, 'opaque'=>1, 'qop'=>1, 'nc'=>1, 'cnonce'=>1);
$data = array();
$parts = explode(',', $digest_str);
foreach ($parts as $part) {
$part = trim($part);
if (preg_match('/(\w+)=["\']?([^"\',]+)["\']?/', $part, $match)) {
$key = $match[1];
$value = $match[2];
if (isset($needed_parts[$key])) {
$data[$key] = $value;
}
}
}
return $data;
}
$realm = 'Digest Test';
$nonce = uniqid();
if (!isset($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Digest realm="' . $realm . '", qop="auth", nonce="' . $nonce . '", opaque="' . md5($realm) . '"');
exit;
}
$digest = parseDigest($_SERVER['PHP_AUTH_DIGEST']);
// Пример проверки: пароль 'password' для пользователя 'user'
$A1 = md5($digest['username'] . ':' . $realm . ':password');
$A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $digest['uri']);
$valid_resp = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $A2);
if ($digest['response'] === $valid_resp) {
echo 'Успешная дайджест-аутентификация';
} else {
header('HTTP/1.0 401 Unauthorized');
echo 'Неверный дайджест';
}
Результат: Браузер отправляет заголовок Authorization: Digest ..., PHP разбирает и проверяет ответ. При совпадении доступа открывается, иначе браузер снова запрашивает пароль.
Пример 4: Логирование неудачных попыток в файл
$log_file = __DIR__ . '/auth_failures.log';
function log_failure($login, $ip) {
global $log_file;
$entry = date('Y-m-d H:i:s') . ' | ' . $ip . ' | Login: ' . $login . PHP_EOL;
file_put_contents($log_file, $entry, FILE_APPEND | LOCK_EX);
}
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Logging"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
$login = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
if ($login !== 'admin' || $pass !== 'strongpw') {
log_failure($login, $_SERVER['REMOTE_ADDR']);
header('HTTP/1.0 401 Unauthorized');
echo 'Доступ запрещён. Попытка зафиксирована.';
exit;
}
echo 'Добро пожаловать, ' . htmlspecialchars($login);
Результат: При каждом неверном вводе данные записываются в файл auth_failures.log. Это помогает отслеживать атаки подбора паролей.