Механизмы переключения между пользователями в PHP администрировании

Раздел: Администрирование 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>';
}
Вывод: предупреждающий блок с возможностью вернуться.

PHP su (переключение пользователя) - comments

En
Php su (php)