Как ограничить подключение к API до одного пользователя?

«Как ограничить подключение к API до одного пользователя?» — вопрос из категории Безопасность, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

На практике я реализовывал это через механизм эксклюзивной блокировки на уровне приложения или базы данных. Основная задача — гарантировать, что в один момент времени с API работает только одна сессия для заданного пользователя или ресурса.

1. Блокировка через Redis (рекомендуемый подход): Используем атомарную операцию SET с флагами NX (только если ключ не существует) и EX (время жизни).

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lockKey = 'api:user:lock:' . $userId;
$lockTimeout = 30; // секунд

// Пытаемся установить блокировку
$isLockAcquired = $redis->set($lockKey, session_id(), ['nx', 'ex' => $lockTimeout]);

if (!$isLockAcquired) {
    // Проверяем, наша ли это блокировка (например, при повторном запросе)
    $currentLockOwner = $redis->get($lockKey);
    if ($currentLockOwner !== session_id()) {
        http_response_code(429);
        header('Retry-After: ' . $lockTimeout);
        echo json_encode(['error' => 'API is currently locked by another user session']);
        exit;
    }
    // Если блокировка наша — обновляем TTL
    $redis->expire($lockKey, $lockTimeout);
}

// Критическая секция API...

// По завершении освобождаем блокировку
$redis->del($lockKey);

2. Блокировка на уровне базы данных (MySQL): Используем таблицу api_locks с проверкой временной метки.

CREATE TABLE api_locks (
    user_id INT PRIMARY KEY,
    lock_token VARCHAR(64),
    locked_until TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
// Попытка захвата блокировки
$query = "INSERT INTO api_locks (user_id, lock_token, locked_until) 
          VALUES (?, ?, NOW() + INTERVAL 30 SECOND)
          ON DUPLICATE KEY UPDATE 
          lock_token = IF(locked_until < NOW(), VALUES(lock_token), lock_token),
          locked_until = IF(locked_until < NOW(), VALUES(locked_until), locked_until)";
// Проверяем, изменилась ли запись и принадлежит ли lock_token текущей сессии

Ключевые моменты:

  • Всегда устанавливайте TTL (Time-To-Live) для блокировки, чтобы избежать вечной блокировки при сбоях.
  • Используйте уникальный токен (например, session_id()) для идентификации владельца блокировки.
  • Для распределенных систем предпочтительнее Redis или специализированные решения (ZooKeeper, etcd).