Ограничение частоты запросов
Форму обратной связи могут засыпать спамом, API — перегрузить запросами, код подтверждения — подобрать перебором. Компонент RateLimiter считает действия и отказывает, когда их слишком много.
Установка и настройка
composer require symfony/rate-limiter# config/packages/rate_limiter.yaml
framework:
rate_limiter:
contact_form:
policy: 'fixed_window'
limit: 5
interval: '10 minutes'Ограничитель contact_form разрешает пять действий за десять минут.
Использование
Symfony создаёт сервис-фабрику, имя аргумента которого складывается из имени ограничителя и слова Limiter:
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\RateLimiter\RateLimiterFactory;
#[Route('/contact', name: 'contact', methods: ['POST'])]
public function send(Request $request, RateLimiterFactory $contactFormLimiter): Response
{
$limiter = $contactFormLimiter->create($request->getClientIp());
if (!$limiter->consume(1)->isAccepted()) {
throw new TooManyRequestsHttpException();
}
// отправка сообщения
}Аргумент create() — ключ, по которому ведётся счёт: адрес посетителя, идентификатор пользователя, адрес почты.
Политики
| Политика | Как считает |
|---|---|
fixed_window | Не больше N действий в каждом отрезке времени. Просто, но на стыке двух отрезков возможен двойной всплеск |
sliding_window | Сглаживает стык: учитывает и предыдущий отрезок |
token_bucket | «Ведро жетонов»: разрешает короткий всплеск, затем пополняется с постоянной скоростью |
framework:
rate_limiter:
api:
policy: 'token_bucket'
limit: 100
rate: { interval: '1 minute', amount: 20 }Клиент может сделать сразу 100 запросов, после чего получает по 20 новых в минуту.
Заголовки ответа
Вежливый API сообщает клиенту, сколько запросов осталось и когда повторить попытку:
$limit = $limiter->consume(1);
$headers = [
'X-RateLimit-Remaining' => $limit->getRemainingTokens(),
'X-RateLimit-Limit' => $limit->getLimit(),
'Retry-After' => $limit->getRetryAfter()->getTimestamp() - time(),
];
if (!$limit->isAccepted()) {
return new JsonResponse(['error' => 'Too many requests'], 429, $headers);
}Ограничение по пользователю
$key = $this->getUser()?->getUserIdentifier() ?? $request->getClientIp();
$limiter = $apiLimiter->create($key);Вошедших пользователей считают по имени, гостей — по адресу.
Где хранятся счётчики
По умолчанию — в кеше приложения. Если серверов несколько, кеш должен быть общим, иначе каждый сервер ведёт свой счёт:
framework:
rate_limiter:
api:
policy: 'sliding_window'
limit: 60
interval: '1 minute'
cache_pool: 'cache.rate_limiter'Сам пул cache.rate_limiter настраивают на Redis в cache.yaml.
Вход на сайт
Для формы входа отдельный код не нужен: настройка login_throttling в security.yaml использует этот же компонент.
Частая ошибка
Если сайт стоит за прокси или балансировщиком, getClientIp() возвращает адрес прокси — один на всех. Ограничение сработает сразу для всех посетителей. Укажите доверенные прокси в настройке framework.trusted_proxies, чтобы Symfony брал настоящий адрес из заголовка X-Forwarded-For.