Ограничение скорости запросов (Rate Limiting) помогает защищать API от злоупотреблений, обеспечивая справедливое использование ресурсов. В этой статье мы разберем, как создать RateLimiter на Yii 1.1 (PHP 7.3) с использованием Redis (^6.2.0).
Стоит отметить, что существуют платные коммерческие решения, такие как Qrator, которые предоставляют комплексные инструменты для защиты API и фильтрации запросов. Однако в этой статье мы сосредоточимся на самостоятельной реализации RateLimiter с использованием открытых инструментов. Также, для простоты и читаемости кода, мы не используем Lua-скрипты и вычисления на стороне Redis.
Что такое Sliding Window Rate Limiter?
Шаблон, который мы реализуем здесь, называется sliding window rate limiter (ограничение скорости с «скользящим окном»). В отличие от fixed window rate limiter (ограничение скорости с фиксированным окном), он накладывает ограничения на запросы в дискретном временном окне, предшествующем текущему запросу.
Разница между Sliding Window и Fixed Window
-
Fixed Window Rate Limiter группирует запросы в строго определённое временное окно. Например, если вы установили ограничение 10 запросов в минуту, то в фиксированном окне может возникнуть ситуация, когда лимитер позволит пройти 20 запросам за одну минуту. Это может произойти, если 10 запросов были отправлены в последние 5 секунд предыдущего окна, а ещё 10 — в первые 5 секунд следующего окна.
-
Sliding Window Rate Limiter, напротив, анализирует запросы, отправленные за последние 60 секунд. Если все 20 запросов были отправлены в пределах 60 секунд друг от друга, лимитер пропустит только 10 из них.
Используя сортированные множества Redis, реализация такого лимитера становится очень простой.
Ключевые возможности
-
Гибкость Sliding Window: Учитываем запросы в скользящем временном окне для точного ограничения скорости.
-
Лимитирование запросов: Устанавливаем максимальное количество запросов на определённое время.
-
Отслеживание запросов с помощью Redis: Используем сортированные множества для хранения временных меток.
Реализация
Вот полный пример реализации
RateLimiter
на PHP.<?php declare(strict_types=1); namespace app\modules\api\filters; use common\helpers\App; use app\Cache\Adapter\PredisAdapter; use app\modules\api\providers\Interfaces\ClientIdProviderInterface; use app\modules\api\providers\Interfaces\ControllerInfoProviderInterface; use Yii; /** * Class RateLimiter */ class RateLimiter extends \CFilter { const MICROSECONDS_FACTOR = 1000000; /** * @var bool */ public $enableHeaders = true; /** * @var int количество запросов */ public $limit; /** * @var int количество секунд */ public $time; /** @var int[] Белый список клиентов */ public $whitelist = []; /** * @var PredisAdapter */ private $redis; /** * @var ClientIdProviderInterface */ private $clientIdProvider; /** * @var ControllerInfoProviderInterface */ private $controllerInfoProvider; public function __construct() { $this->redis = Yii::app()->cache->getClient(); $this->clientIdProvider = Yii::app()->container->get(ClientIdProviderInterface::class); $this->controllerInfoProvider = Yii::app()->container->get(ControllerInfoProviderInterface::class); } /** * {@inheritdoc} * @throws \CHttpException */ public function preFilter($filterChain): bool { $clientId = $this->clientIdProvider->getCurrentClientId(); $action = $this->controllerInfoProvider->getActionId($filterChain); $key = $this->getRedisKey($clientId, $action); $totalMicroseconds = intval(microtime(true) * self::MICROSECONDS_FACTOR) // Удаление старых запросов $this->redis->zremrangebyscore($key, '-inf', $totalMicroseconds - $this->time * self::MICROSECONDS_FACTOR); // Проверяем whitelist if (in_array($clientId, $this->whitelist)) { $this->saveRequest($key, $totalMicroseconds); return true; } $requestCount = $this->redis->zcard($key); $allowance = $requestCount + 1; if ($allowance > $this->limit) { $this->saveRequest($key, $totalMicroseconds); $this->addHeaders($this->limit, 0, $this->time); throw new \CHttpException(429, 'Слишком много запросов'); } $this->saveRequest($key, $totalMicroseconds); $this->addHeaders($this->limit, $this->limit - $allowance, (int)((($this->limit - $allowance + 1) * $this->time) / $this->limit)); return true; } private function saveRequest(string $key, int $timestamp): void { $this->redis->zadd($key, [$timestamp => $timestamp]); $this->redis->expire($key, $this->time); } public function getRedisKey(int $clientId, string $action): string { return "rate_limit:{$clientId}:{$action}"; } private function addHeaders(int $limit, int $remaining, int $reset): void { if ($this->enableHeaders && !App::isTest()) { header('X-Rate-Limit-Limit: ' . $limit); header('X-Rate-Limit-Remaining: ' . $remaining); header('X-Rate-Limit-Reset: ' . $reset); } } }
Подключение фильтра к контроллеру
В контроллере подключите фильтр:
<?php declare(strict_types=1); namespace app\controllers; use yii\web\Controller; use app\modules\api\filters\RateLimiter; class ApiController extends Controller { /** * {@inheritdoc} */ public function filters() { return array_merge( parent::filters(), [ [ RateLimiter::class, 'limit' => 10, 'time' => 60, 'whitelist' => Yii::app()->params['api_rate_limit_whitelist'], ], ] ); } public function actionIndex() { return ['message' => 'Welcome to the API']; } }
Как это работает
-
Определение пользователя: Лимитер определяет текущего пользователя через
getCurrentClientId
и выполняемое действие API. -
Хранение запросов в Redis: Запросы сохраняются в сортированном множестве, где временные метки выступают в роли ключей и значений.
-
Очистка старых запросов: Старые записи за пределами временного окна удаляются с помощью команды
zremrangebyscore
. -
Проверка лимита: Количество запросов (
zcard
) сравнивается с заданным лимитом. -
Добавление заголовков: Заголовки возвращаются с информацией о текущем лимите.
Данные в Redis
Ключ для каждого пользователя и действия выглядит так:rate_limit:{clientId}:{action}
Пример хранимых данных:
[ 1726823063987469 => 1726823063987469, 1726823063987479 => 1726823063987479 ]
-
Возможные улучшения
-
Динамические лимиты: Устанавливайте индивидуальные лимиты для пользователей в зависимости от их уровня доступа.
-
Глобальные ограничения: Добавьте глобальный лимит для всего API.
-
Аналитика: Используйте данные запросов для анализа активности или выявления злоупотреблений.
Заключение
Этот RateLimiter реализован просто, эффективно и легко расширяется. Использование Redis обеспечивает высокую производительность и надежную обработку конкурентных запросов. Независимо от того, создаёте ли вы публичное API или внутренний инструмент, такой подход поможет защитить ресурсы и обеспечить их справедливое использование.
Как вы используете RateLimiter в своих проектах? Делитесь своими мыслями и опытом в комментариях! 😊
ссылка на оригинал статьи https://habr.com/ru/articles/871936/
Добавить комментарий