Механизмы переключения между пользователями в PHP администрировании
Реализация переключения между учетными записями пользователей в PHP
Как организовать временную смену пользователя в админке для отладки и поддержки?
Наиболее эффективный способ - управление сессионными данными с сохранением контекста администратора. При переключении создается резервная копия идентификатора администратора, затем сессия перезаписывается данными целевого пользователя. В сессию добавляется флаг su_active и метаданные (время, кто переключился). Возврат осуществляется очисткой флага и восстановлением исходных данных из резерва.
// Файл: switch_user.php
session_start();
if ($_GET['action'] === 'switch' && isset($_GET['user_id'])) {
// Проверка прав администратора
if (!$_SESSION['is_admin']) {
die('Доступ запрещен');
}
// Сохраняем текущего администратора
$_SESSION['su_original'] = [
'user_id' => $_SESSION['user_id'],
'username' => $_SESSION['username'],
'role' => $_SESSION['role']
];
// Получаем данные целевого пользователя из БД
$targetUser = getUserById((int)$_GET['user_id']);
// Устанавливаем сессию как у целевого пользователя
$_SESSION['user_id'] = $targetUser['id'];
$_SESSION['username'] = $targetUser['name'];
$_SESSION['role'] = $targetUser['role'];
$_SESSION['su_active'] = true;
$_SESSION['su_switched_at'] = time();
// Логируем действие
logAction($_SESSION['su_original']['user_id'], 'su_switch', $targetUser['id']);
header('Location: /dashboard');
exit;
}
if ($_GET['action'] === 'restore') {
if (!empty($_SESSION['su_original'])) {
// Восстанавливаем данные администратора
$_SESSION['user_id'] = $_SESSION['su_original']['user_id'];
$_SESSION['username'] = $_SESSION['su_original']['username'];
$_SESSION['role'] = $_SESSION['su_original']['role'];
unset($_SESSION['su_original'], $_SESSION['su_active'], $_SESSION['su_switched_at']);
logAction($_SESSION['user_id'], 'su_restore');
}
header('Location: /admin');
exit;
}
Пояснение: после переключения все запросы исполняются с правами целевого пользователя, но с полным логированием. Резервное копирование хранится в сессии и удаляется при возврате. Флаг su_active позволяет модифицировать поведение интерфейса (например, добавить панель с кнопкой “Вернуться”).
Возможные проблемы:
- Потеря резервной копии при истечении сессии (решение - хранить в базе данных).
- Несанкционированное переключение (необходимы строгие проверки прав).
- Конфликт с другими модулями, которые сбрасывают сессию (кэширование, CSRF-защита).
Типичная ошибка: использование одного и того же идентификатора сессии для админа и пользователя - приводит к невозможности вернуться. Решение: всегда сохранять резервную копию до перезаписи.
Вариант 1. Одноразовый токен в ссылке - как сделать безопасное переключение без сессионных рисков?
Администратор генерирует временный URL с хэшированным токеном (например, HMAC), который привязан к идентификатору целевого пользователя и времени жизни. При переходе по ссылке токен проверяется, создается временная сессия пользователя без сохранения данных администратора.
// Генерация токена
$secret = 'my_secret_key';
$userId = 42;
$expires = time() + 3600;
$token = hash_hmac('sha256', $userId . '|' . $expires, $secret);
$link = "https://site.com/su?user_id=$userId&token=$token&exp=$expires";
Цель: применение для одноразовой передачи доступа другому администратору без раскрытия пароля. Проблема - токен может быть перехвачен (использовать HTTPS).
Вариант 2. Использование exec('su') в консольных скриптах - как выполнить PHP скрипт от имени другого системного пользователя?
В CLI-сценариях (например, crontab) можно вызвать exec('su -c "php /path/script.php" targetuser'). Однако это требует соответствующих прав в sudoers. Альтернатива - posix_setuid() (доступно только root).
// Пример с posix_setuid (только для root)
$uid = posix_getpwnam('www-data')['uid'];
if (posix_setuid($uid) === false) {
die('Не удалось сменить пользователя');
}
// Дальнейший код выполняется от имени www-data
Цель: запуск длительных процессов или задач с четко определенными правами файловой системы. Ошибки: вызов без прав root приводит к ошибке, а exec('su') может требовать TTY.
Вариант 3. Плагины для CMS (WordPress, Joomla) - как реализовать переключение без переписывания ядра?
В WordPress используется плагин “User Switching” (by John Blackbourn), который сохраняет оригинальный идентификатор в метаданных сессии и переключает пользователя через wp_set_current_user(). Аналогичный подход для Joomla - расширение “Switch User”. Установка и настройка не требуют кода, но дают гибкость.
Проблемы: совместимость с другими плагинами, возможное дублирование сессий. Решение - использовать встроенные хуки для логирования.
Цели использования каждого варианта:
- Основное решение - требуется полный контроль и кастомизация в самописной системе.
- Токен - быстрая передача доступа на время.
- exec/posix - системные операции в CLI.
- Плагины - готовое решение для популярных CMS с минимальными усилиями.
Расширенные примеры и нестандартные сценарии
Пример 1. Класс для управления переключением с поддержкой истории
class UserSwitcher {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
public function switchTo(int $targetUserId): array {
session_start();
if (!$_SESSION['is_admin']) {
return ['success' => false, 'error' => 'Access denied'];
}
// Сохраняем оригинал в БД
$stmt = $this->db->prepare('INSERT INTO su_log (admin_id, target_id, switched_at, restored) VALUES (?, ?, NOW(), 0)');
$stmt->execute([$_SESSION['user_id'], $targetUserId]);
$logId = $this->db->lastInsertId();
$_SESSION['su_active'] = true;
$_SESSION['su_original'] = [
'user_id' => $_SESSION['user_id'],
'username' => $_SESSION['username'],
'role' => $_SESSION['role'],
'log_id' => $logId
];
// Загружаем данные целевого пользователя
$target = $this->loadUser($targetUserId);
$_SESSION['user_id'] = $target['id'];
$_SESSION['username'] = $target['name'];
$_SESSION['role'] = $target['role'];
$_SESSION['su_log_id'] = $logId;
return ['success' => true];
}
public function restore(): array {
if (empty($_SESSION['su_original'])) {
return ['success' => false, 'error' => 'No session to restore'];
}
// Восстанавливаем из оригинала
$original = $_SESSION['su_original'];
$_SESSION['user_id'] = $original['user_id'];
$_SESSION['username'] = $original['username'];
$_SESSION['role'] = $original['role'];
// Обновляем запись в БД
$stmt = $this->db->prepare('UPDATE su_log SET restored = 1, restored_at = NOW() WHERE id = ?');
$stmt->execute([$_SESSION['su_log_id']]);
unset($_SESSION['su_active'], $_SESSION['su_original'], $_SESSION['su_log_id']);
return ['success' => true];
}
}
Результат работы: при вызове $switcher->switchTo(5) администратор получает сессию пользователя с id=5, а в таблице su_log фиксируется запись. При restore() сессия возвращается, запись помечается как восстановленная.
Пример 2. Одноразовый токен с проверкой IP
// Создание токена с привязкой к IP
$ip = $_SERVER['REMOTE_ADDR'];
$data = $targetUserId . '|' . $expires . '|' . $ip;
$token = hash_hmac('sha256', $data, $secret);
// Ссылка: /su?user_id=$targetUserId&token=$token&exp=$expires
// Проверка
$expectedToken = hash_hmac('sha256', $targetUserId . '|' . $expires . '|' . $_SERVER['REMOTE_ADDR'], $secret);
if (!hash_equals($expectedToken, $_GET['token']) || $expires < time()) {
die('Недействительная ссылка');
}
Результат: ссылка работает только для того IP, с которого была сгенерирована, истекает через заданное время.
Пример 3. Переключение пользователя через console с логированием в syslog
#!/usr/bin/php
// su_script.php - запуск от root
$targetUser = $argv[1] ?? 'nobody';
$uid = posix_getpwnam($targetUser)['uid'];
if (!posix_setuid($uid)) {
syslog(LOG_ERR, "Failed to setuid to $targetUser");
exit(1);
}
syslog(LOG_INFO, "Running as $targetUser");
// выполнение задач...
Вывод в syslog: "Running as www-data", ошибки при неверном имени пользователя.
Пример 4. Использование плагина User Switching в WordPress (вывод кнопки)
// В functions.php темы
add_filter('user_row_actions', function($actions, $user) {
if (current_user_can('administrator')) {
$actions['switch_to'] = '<a href="' . wp_nonce_url("users.php?action=switch_to_user&user_id=$user->ID", "switch-to-user_$user->ID") . '">Переключиться</a>';
}
return $actions;
}, 10, 2);
Результат: в списке пользователей появляется ссылка “Переключиться” только для администратора.
Пример 5. Отображение статуса переключения в интерфейсе
if (!empty($_SESSION['su_active'])) {
echo '<div class="alert alert-warning">Вы вошли как ' . htmlspecialchars($_SESSION['username']) .
' (<a href="?action=restore">вернуться к ' . htmlspecialchars($_SESSION['su_original']['username']) . '</a>)</div>';
}
Вывод: предупреждающий блок с возможностью вернуться.