Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Ограничение частоты запросов

Форму обратной связи могут засыпать спамом, 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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.