36 KiB
Аутентификация и сеансы
XC_VM поддерживает три контекста аутентификации - администратора, реселлера и игрока - каждый с изолированными сеансовыми ключами, отдельными потоками входа в систему и независимой логикой проверки. В этом документе описан полный жизненный цикл аутентификации от входа в систему до проверки сеанса и обеспечения безопасности.
Обзор процесса входа в систему
Все три контекста следуют схожему шаблону высокого уровня, с зависящими от контекста различиями в проверке и хранении сеанса.
Взаимодействие администратора и реселлера
POST request with credentials
-> BruteforceGuard checks (flood / brute-force)
-> Optional reCAPTCHA verification
-> Credential lookup via UserRepository::getAuthUserByCredentials()
-> Access code / group validation
-> Permission check (is_admin or is_reseller)
-> User status check (enabled/disabled)
-> Password re-hash + session write + login log
Поток игроков
POST request with credentials
-> UserRepository::getUserInfo() lookup
-> Line type checks (reject E2, MAG, Stalker)
-> Expiration date check
-> admin_enabled / enabled status checks
-> IP allowlist, country restriction, user agent, ISP checks
-> Session write + redirect
-> BruteforceGuard::checkFlood() on any failure
Аутентификатор
Файл: src/Core/Auth/Authenticator.php
Authenticator::login(array $data, bool $bypassRecaptcha = false): array
Способ входа в систему администратора. Шаги по порядку:
- Проверка повторной проверки (если параметр
recaptcha_enableвключен и не пропущен). - Поиск учетных данных с помощью
UserRepository::getAuthUserByCredentials(). - Проверка группы кодов доступа - имя пользователя
member_group_idдолжно входить в разрешенные группы текущего кода доступа, в противном случае кодов доступа не должно существовать. - Permission check --
is_adminmust be true for the user's group. - Проверка состояния -
$rUserInfo['status'] == 1(включено). - В случае успеха: повторно хэширует пароль, обновляет
last_loginиipв базе данных, переносит сеанс на новый идентификатор (session_regenerate_id(true)), записывает ключи сеанса, регистрирует логин.
Новый идентификатор имеет значение: идентификатор, с которым посетитель вводит форму входа в систему, может быть известен кому-то еще (файл cookie, установленный с дочернего поддомена, общего компьютера), и, сохранив его, пользователь также войдет в систему. resellerLogin() и страница настройки при первом запуске делает то же самое.
Значения сеанса, записанные в зависимости от успеха:
$_SESSION['hash'] = $rUserInfo['id']; // User ID
$_SESSION['ip'] = $rIP; // Client IP at login
$_SESSION['code'] = AuthRepository::getCurrentCode(); // Current access code
$_SESSION['verify'] = md5($rUserInfo['username'] . '||' . $rCrypt); // Verification hash
Authenticator::resellerLogin(array $data): array
Способ входа в систему реселлера. Структура, идентичная login(), с этими различиями:
- Повторная проверка всегда проверяется, когда она включена (параметр обхода отсутствует).
- Для проверки разрешений требуется
is_resellerвместоis_admin. - Возвращает
STATUS_NOT_RESELLER, если у пользователя нет разрешения торгового посредника. - Логи входа в систему записываются с типом
RESELLERвместоADMIN.
Значения сеанса, записанные в зависимости от успеха:
$_SESSION['reseller'] = $rUserInfo['id']; // User ID
$_SESSION['rip'] = $rIP; // Client IP at login
$_SESSION['rcode'] = AuthRepository::getCurrentCode(); // Current access code
$_SESSION['rverify'] = md5($rUserInfo['username'] . '||' . $rCrypt); // Verification hash
Константы статуса входа в систему
Определено в src/bootstrap.php через XC_Bootstrap::defineStatusConstants():
| Постоянный | Ценность | Значение |
|---|---|---|
STATUS_FAILURE |
0 | Общий сбой (неверные учетные данные или универсальная ошибка) |
STATUS_SUCCESS |
1 | Вход в систему был успешным |
STATUS_DISABLED |
5 | Учетная запись отключена |
STATUS_NOT_ADMIN |
6 | У пользователя нет прав администратора |
STATUS_INVALID_CAPTCHA |
12 | Не удалось выполнить проверку reCAPTCHA |
STATUS_INVALID_CODE |
13 | Несоответствие кода доступа и группы |
STATUS_NOT_RESELLER |
35 | У пользователя нет разрешения торгового посредника |
Хэширование паролей
Authenticator::hashPassword(string $password, ?string $salt = null, int $rounds = 20000): string
Использует crypt() с SHA-512 ($6$). Используется формат $6$rounds=20000$<salt>$, где <salt> - это 16 шестнадцатеричных символов, полученных из openssl_random_pseudo_bytes(16). Пароли повторно хэшируются при каждом успешном входе в систему, что приводит к перераспределению ресурсов.
Authenticator::checkPassword(string $password, string $storedHash): bool
Проверяет открытый текстовый пароль на соответствие сохраненному хэшу, используя crypt($password, $storedHash), с безопасным по времени сравнением с помощью hash_equals(). Сохраненный хэш содержит алгоритм, раунды и соль, поэтому crypt() воспроизводит правильный хэш для сравнения.
Аутентификация игрока
Файл: src/Public/Controllers/Player/PlayerLoginController.php
Процедура входа игрока в систему принципиально отличается от процедуры администратора/реселлера. Она позволяет аутентифицировать "линии" конечного пользователя (подписки на IPTV), а не операторов панели.
Процесс входа в систему
PlayerLoginController::processLogin() выполняет эти проверки в порядке:
- Поиск учетных данных --
UserRepository::getUserInfo()(отличается отgetAuthUserByCredentials, используемого администратором/реселлером). - Отклонение типа линии -- Линии E2, MAG и Stalker отклоняются с определенными кодами ошибок.
- Проверка истечения срока годности --
exp_dateдолжно быть равно null или в будущем. - Проверка с поддержкой администратора --
admin_enabled == 0возвращаетCLIENT_BANNED. - Проверка, включенная пользователем --
enabled == 0возвращаетCLIENT_DISABLED. - Список разрешенных IP-адресов -- Если для пользователя задано значение
allowed_ips, IP-адрес клиента должен совпадать (решается с помощьюgethostbyname). - Ограничение по стране - Два режима:
- Для каждого пользователя: если задано значение
forced_country, а неALL, страна GeoIP должна совпадать. - Глобальный: если нет переопределения для каждого пользователя, устанавливается глобальный параметр
allow_countries(если только он не содержитALL).
- Для каждого пользователя: если задано значение
- Проверка агента пользователя -- Если для пользователя задано значение
allowed_ua, то пользовательский агент HTTP должен соответствовать. - флаг Проверка интернет-провайдера --
isp_violateотклоняет соединение. - Проверка сервера интернет-провайдера -- Если значение
isp_is_serverравно true и пользователь не является рестримером, соединение будет отклонено.
Каждый сбой запускает BruteforceGuard::checkFlood() перед возвратом кода ошибки.
Коды ошибок проигрывателя
| Постоянный | Ценность | Значение |
|---|---|---|
CLIENT_INVALID |
0 | Неверное имя пользователя или пароль |
CLIENT_IS_E2 |
1 | Линии Enigma запрещены |
CLIENT_IS_MAG |
2 | МАГНИТНЫЕ линии не допускаются |
CLIENT_IS_STALKER |
3 | Линии преследования запрещены |
CLIENT_EXPIRED |
4 | Срок действия строки истек |
CLIENT_BANNED |
5 | Строка заблокирована (admin_enabled = 0) |
CLIENT_DISABLED |
6 | Линия отключена (включено = 0) |
CLIENT_DISALLOWED |
7 | Не удалось выполнить проверку IP/страны/UA/провайдера |
Ключи к сеансу игрока
$_SESSION['phash'] = $rUserInfo['id'];
$_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password']);
В контексте player хранятся только два сеансовых ключа. В отличие от ключей администратора и реселлера, здесь нет ключей activity, ip или code. Это означает, что в сеансе player нет таймаута бездействия и не обнаруживается изменение IP-адреса на уровне сеанса.
Проверка сеанса при загрузке страницы
После первоначального входа в систему при каждой загрузке страницы, прошедшей проверку подлинности, выполняется повторная проверка сеанса. Это происходит в файлах начальной загрузки, а не в SessionManager.
Проверка сеанса администратора
Файл: src/Public/Views/admin/functions.php
Если установлено значение $_SESSION['hash'], при каждой загрузке страницы выполняются следующие проверки:
- Поиск пользователя --
UserRepository::getRegisteredUserById($_SESSION['hash']). Если пользователь больше не существует, сеанс завершается. - Проверка прав доступа --
AuthRepository::getPermissions()должен возвращать допустимый набор сis_admin == true. - Проверка IP-адреса - Сравнивает текущий IP-адрес с
$_SESSION['ip']:- Если параметр
ip_subnet_matchвключен: сравниваются только первые три октета (например,192.168.1.*соответствует192.168.1.*). - Если параметр
ip_subnet_matchотключен: требуется точное совпадение IP-адресов. - Если IP-адрес не совпадает и включена настройка
ip_logout, сеанс завершается. - Если IP-адрес не совпадает и
ip_logoutотключен,$_SESSION['ip']автоматически обновляется до нового IP-адреса.
- Если параметр
- Проверить проверку хэша Verify --
$_SESSION['verify']должно быть равноmd5($rUserInfo['username'] . '||' . $rUserInfo['password']). Это гарантирует, что сеанс будет аннулирован в случае изменения пароля.
Если какая-либо проверка завершается неудачей, сеанс очищается с помощью SessionManager::clearContext('admin'), и пользователь перенаправляется на индексную страницу.
Проверка сеанса работы с реселлером
Файл: src/Infrastructure/Bootstrap/reseller_functions.php
Логика идентична проверке администратора, но используются сеансовые ключи реселлера:
- Проверяет
$_SESSION['reseller']для идентификатора пользователя. - Использует
$_SESSION['rip']для сравнения IP-адресов. - Использует
$_SESSION['rverify']для проверки хэша. - Проверяет разрешение
is_resellerвместоis_admin.
Соответствие IP-подсети и поведение при выходе из системы по IP-адресу такое же, как у администратора.
Тайм-аут сеанса администрирования
Файл: src/Public/Views/admin/session.php
Для сеансов администрирования выполняется отдельная проверка времени ожидания сеанса. Если заданы значения $_SESSION['hash'] и $_SESSION['last_activity'], а с момента last_activity прошло более 60 минут, то сеансовые ключи (hash, ip, code, verify, last_activity) не заданы. При каждом действительном запросе обновляется $_SESSION['last_activity'], и сессия закрывается для записи.
Проверка сеанса игрока
Контекст проигрывателя не выполняет проверку IP-адреса, соответствие подсети или тайм-аут активности на уровне сеанса. Сохраняются только phash и pverify, и проверка выполняется на прикладном уровне для повторной проверки этих значений по базе данных.
Менеджер сеанса
Файл: src/Core/Auth/SessionManager.php
Унифицированный сеансовый API, который абстрагирует различные имена сеансовых ключей в разных контекстах. Предназначен для замены устаревших файлов admin/session.php и reseller/session.php.
Контекстная ключевая карта
| Логический ключ | Ключ администратора $_SESSION |
Ключ реселлера $_SESSION |
Ключ игрока $_SESSION |
|---|---|---|---|
auth |
hash |
reseller |
phash |
activity |
last_activity |
rlast_activity |
-- |
ip |
ip |
rip |
-- |
code |
code |
rcode |
-- |
verify |
verify |
rverify |
pverify |
Контекст игрока намеренно опускает сопоставления activity, ip и code.
Методы
start(string $context, int $timeout = 60): void
Запускает сеанс PHP (если он еще не запущен), устанавливает активный контекст и запускает checkTimeout() для завершения устаревших сеансов. Контекст должен быть 'admin', 'reseller' или 'player'.
requireAuth(?string $loginUrl = null): void
Проверяет наличие аутентифицированного сеанса. Если запрос направлен напрямую на session.php, возвращает ответ в формате JSON {"result": true/false} (используется для опроса сеанса AJAX). В противном случае перенаправляет не прошедших проверку пользователей на страницу входа в систему. В случае успеха вызывает touch() для обновления временной метки действия.
isAuthenticated(): bool
Проверка на отсутствие блокировки. Возвращает значение true, если сеанс был запущен и установлен ключ auth.
getUser(): mixed
Возвращает значение, сохраненное в ключе сеанса auth (идентификатор пользователя для администратора/реселлера или идентификатор строки для игрока), или null, если аутентификация не пройдена.
getValue(string $name): mixed
Возвращает значение сеанса по его логическому имени (auth, activity, ip, code, verify). Логическое имя сопоставляется с фактическим ключом $_SESSION на основе текущего контекста.
setValue(string $name, mixed $value): void
Устанавливает значение сеанса по логическому имени.
login(mixed $hash, ?string $ip = null): void
Создает аутентифицированный сеанс, задавая значения auth и activity. При необходимости сохраняет IP-адрес клиента.
destroy(): void
Очищает все ключи сеанса для текущего контекста. Если никакой другой контекст не активен (проверяет противоположный контекст администратора/реселлера), уничтожает весь сеанс PHP.
clearContext(string $context): void
Очищает все ключи сеанса для определенного контекста, не разрушая сеанс. Простая замена устаревшего destroySession($type).
touch(): void
Обновляет $_SESSION[$activityKey] до текущей временной метки и вызывает session_write_close(), чтобы снять блокировку сеанса.
getContext(): ?string
Возвращает текущую контекстную строку ('admin', 'reseller', 'player') или null, если она не задана.
Поведение по истечении времени ожидания
SessionManager::DEFAULT_TIMEOUT равно 60 минутам. Метод checkTimeout() (вызываемый автоматически start()) сравнивает время, прошедшее с момента last_activity. Если время ожидания превышено, все ключи сеанса, зависящие от контекста, сбрасываются, что приводит к выходу пользователя из системы.
Поскольку контекст игрока не имеет ключа activity в карте ключей, проверка тайм-аута не применяется к сессиям игрока.
Охранник с применением грубой силы
Файл: src/Core/Auth/BruteforceGuard.php
Централизованное ограничение скорости и защита от перебора. Все методы используют состояние на основе файла, хранящееся в FLOOD_TMP_PATH (/home/xc_vm/tmp/flood/). Разрешенные IP-адреса (IP-адреса сервера) и IP-адреса, указанные в параметре flood_ips_exclude, всегда исключаются.
checkFlood(?string $ip = null, bool $useCachedMode = false): void
Скорость - ограничивает количество запросов по IP-адресу в пределах настраиваемого временного интервала.
- Настройки:
flood_limit(максимальное количество запросов),flood_seconds(размер окна). - Файл состояния:
FLOOD_TMP_PATH . $ip- сохраняет объект JSON с числомrequestsи временной меткойlast_request. - Поведение: Отслеживает количество запросов в пределах временного окна. Если количество превышает
flood_limit, IP-адрес блокируется (заносится в таблицуblocked_ipsили передается через Redis в кэшированном/потоковом режиме). Файл состояния удаляется после блокировки. - Используется: Вход игрока (вызывается при каждой неудачной попытке входа в систему), конечные точки потоковой передачи.
checkBruteforce(?string $ip = null, ?string $mac = null, ?string $username = null, bool $useCachedMode = false): void
Обнаруживает атаки методом перебора на основе количества уникальных MAC-адресов или имен пользователей, обнаруженных с одного IP-адреса.
- Настройки:
bruteforce_mac_attempts,bruteforce_username_attempts( максимальное количество уникальных значений),bruteforce_frequency(временной интервал в секундах). - Файл состояния:
FLOOD_TMP_PATH . $ip . '_mac'илиFLOOD_TMP_PATH . $ip . '_user'- сохраняет попытки в виде пар{term: timestamp}. - Поведение: Попытки с истекшим сроком действия (за пределами частотного диапазона) отсекаются с помощью
truncateAttempts(). Если количество уникальных запросов превышает допустимое, IP-адрес блокируется. - Используется: Конечные точки потоковой аутентификации.
checkAuthFlood(array $user, ?string $ip = null): void
Ограничивает скорость запросов на аутентификацию для конкретной комбинации пользователь +IP. Предназначен для ограничения повторных попыток авторизации без полной блокировки.
- Настройки:
auth_flood_limit(максимальное количество попыток),auth_flood_seconds(окно),auth_flood_sleep(задержка в секундах при блокировке). - Файл состояния:
FLOOD_TMP_PATH . $userId . '_' . $ip- сохраняет попытки в виде индексированных временных меток плюс необязательную временную меткуblock_until. - Поведение: Когда количество попыток превышает допустимое значение, устанавливается временная метка
block_until. Последующие запросы в течение периода блокировки задерживаются наauth_flood_sleepсекунды (черезsleep()). IP-адрес не блокируется навсегда. Пользователи Restreamer (is_restreamer) освобождаются от этого требования. - Используется: Потоковая аутентификация.
truncateAttempts(array $attempts, int $frequency, bool $list = false): array
Отфильтровывает просроченные попытки из массива отслеживания. Если значение $list равно true, массив обрабатывается как индексированный (для checkAuthFlood); в противном случае как ассоциативный, с ключом по термину (для checkBruteforce).
Блокирующий механизм
Когда IP-адрес заблокирован:
- Обычный режим: Выполняет вставку в таблицу базы данных
blocked_ipsс указанием причины (FLOOD ATTACKилиBRUTEFORCE MAC/USER ATTACK) и обновляет кэшBlocklistService. - Режим кэширования/потоковой передачи (
$useCachedMode = true): Устанавливает сигнал Redis (bruteforce_attack/$ipилиflood_attack/$ip) черезRedisManager::setSignal()для блокировки потокового контекста без записи в базу данных. - В обоих режимах выполняется касание файла-маркера
FLOOD_TMP_PATH . 'block_' . $ipдля быстрой проверки на уровне файловой системы.
Безопасность сеанса
Настройка файлов cookie
В контексте начальной загрузки администратора сеансовый файл cookie имеет значения SameSite=Strict и HttpOnly, а PHP работает в строгом режиме, отказываясь от идентификаторов сеанса, которые он никогда не выдавал:
$params['samesite'] = 'Strict';
$params['httponly'] = true;
session_set_cookie_params($params);
ini_set('session.use_strict_mode', '1');
session_start();
Ни один скрипт панели не считывает сессионный файл cookie, поэтому HttpOnly ничего не стоит и не позволяет XSS прочитать его.
Проверка хэша
Проверяющий хэш ($_SESSION['verify'] / $_SESSION['rverify'] / $_SESSION['pverify']) вычисляется следующим образом:
md5($username . '||' . $hashedPassword)
Это значение сравнивается с текущими значениями базы данных при каждой загрузке страницы. Если администратор изменяет пароль пользователя (что изменяет сохраненный хэш), все существующие сеансы для этого пользователя автоматически становятся недействительными, поскольку проверяемый хэш больше не будет совпадать.
Для логинов администратора и торгового посредника пароль повторно хэшируется во время входа в систему, поэтому используется $rCrypt (новый хэш). Для логинов игроков используется существующий сохраненный хэш $rUserInfo['password'].
Обработка изменений IP-адреса
Два параметра управляют поведением при изменении IP-адреса:
| Установка | Эффект |
|---|---|
ip_logout |
Если включено, завершает сеанс при изменении IP-адреса клиента (точного или из подсети, в зависимости от ip_subnet_match). |
ip_subnet_match |
Когда этот параметр включен, сравниваются только первые три октета IP-адреса, вместо того чтобы требовать точного совпадения. Позволяет пользователям с динамическими IP-адресами в пределах одной подсети сохранять сеанс связи. |
Когда ip_logout отключено и IP-адрес изменяется, сохраненный IP-адрес сеанса автоматически обновляется до нового IP-адреса.
Регистрация входа в систему
Неудачные входы в систему (INVALID_LOGIN) всегда регистрируются, поскольку они учитываются в соответствии с лимитом потока входов. Если параметр save_login_logs включен, все остальные результаты также записываются. Все это заносится в таблицу login_logs:
INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`)
VALUES($type, $codeId, $userId, $status, $ip, $timestamp);
| Колонка | Описание |
|---|---|
type |
ADMIN или RESELLER |
access_code |
Идентификатор текущего кода доступа |
user_id |
Идентификатор пользователя (0 для неверных учетных данных) |
status |
SUCCESS, INVALID_LOGIN, INVALID_CODE, NOT_ADMIN, DISABLED |
login_ip |
IP-адрес клиента |
date |
Временная метка Unix |
Логины игроков не записываются в login_logs.
Ограничение потока данных для входа в систему
Страницы входа администратора и реселлера запрашивают Authenticator::loginFloodExceeded($ip, $rSettings['login_flood']) перед обработкой входа. Если адрес содержит login_flood или более INVALID_LOGIN строк, датированных в течение последних 24 часов, он добавляется в список заблокированных (LOGIN FLOOD ATTACK), и запрос завершается. Значение login_flood из 0 отменяет ограничение. Быстрые инструменты → Очистить поток данных для входа в систему удаляет подсчитанные строки.
date - это временная метка Unix, поэтому значение окна равно date >= time() - 86400. Страницы, используемые для фильтрации, имеют значение TIME_TO_SEC(TIMEDIFF(NOW(), date)), которое равно нулю для целочисленного столбца, поэтому ни один адрес не был заблокирован.
Авторизация (после входа в систему)
После аутентификации два дополнительных уровня авторизации определяют, к чему пользователь может получить доступ:
Authorization
Файл: src/Core/Auth/Authorization.php
Авторизация на уровне объекта. Проверяет, есть ли у текущего пользователя разрешение на доступ к определенному ресурсу (пользователь, поток и т.д.) на основе иерархии владельцев реселлеров и групповых разрешений.
Authorization::hasResellerPermissions($type)-- устанавливает флажок единственного разрешения на$rPermissions.Authorization::check($type, $id)-- проверяет доступ к определенному ресурсу по типу и идентификатору.
PageAuthorization
Файл: src/Core/Auth/PageAuthorization.php
Управление доступом на уровне страницы. Определяет, разрешают ли групповые разрешения текущего пользователя доступ к определенной странице панели администратора или торгового посредника.
PageAuthorization::checkResellerPermissions($page)- сопоставляет названия страниц с флагами требуемых разрешений и возвращает, разрешен ли доступ.
Связанные файлы
| Файл | Цель |
|---|---|
src/Core/Auth/Authenticator.php |
Логика входа администратора и реселлера в систему, хэширование паролей |
src/Core/Auth/SessionManager.php |
Унифицированный сеансовый API с сопоставлением контекстных ключей |
src/Core/Auth/BruteforceGuard.php |
Ограничение скорости и защита от перебора |
src/Core/Auth/Authorization.php |
Проверки авторизации на уровне объекта |
src/Core/Auth/PageAuthorization.php |
Управление доступом на уровне страницы |
src/Public/Controllers/Player/PlayerLoginController.php |
Процесс входа игрока в систему с проверкой безопасности |
src/Public/Views/admin/functions.php |
Проверка сеанса администратора при каждой загрузке страницы |
src/Public/Views/admin/session.php |
Тайм-аут сеанса администратора и проверка сеанса AJAX |
src/Infrastructure/Bootstrap/reseller_functions.php |
Проверка сеанса реселлера при каждой загрузке страницы |
src/Domain/User/UserRepository.php |
Поиск учетных данных (getAuthUserByCredentials) |
src/bootstrap.php |
Определения констант состояния, контексты начальной загрузки |
src/Core/Config/ConstantsInitializer.php |
FLOOD_TMP_PATH определение (paths() карта) |