Skip to content

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

Ограничение по маршруту, через middleware throttle.

php
Route::post('form/submit', 'FormController@handle')->middleware('throttle');

Три способа задать лимит

ЗаписьЗначит
throttle60 запросов в минуту
throttle:2020 в минуту
throttle:100,5100 за 5 минут
throttle:loginименованный лимитер login

Встроенных форм хватает в большинстве случаев. Именованный нужен, когда лимит зависит от запроса.

Именованные лимитеры

Объявляется один раз, дальше используется по имени:

php
use Boshnik\PageBlocks\Routing\Limit;
use Boshnik\PageBlocks\Support\RateLimiter;

RateLimiter::for('login', function ($request) {
    return Limit::perMinute(5)->by($request->ip());
});
php
Route::post('login', 'AuthController@login')->middleware('throttle:login');

Как собрать лимит

php
Limit::perSecond(2);          // 2 в секунду
Limit::perMinute(60);         // 60 в минуту
Limit::perMinutes(5, 100);    // 100 за 5 минут
Limit::perHour(1000);         // 1000 в час
Limit::perDay(5000);          // 5000 в сутки
Limit::none();                // без ограничения

->by($key) выбирает корзину — IP, id пользователя, токен API. Без него корзина — это IP вызывающего.

Limit::none() — способ лимитера сказать «этого не считаем»: возврат из колбэка пропускает запрос, и читается это лучше, чем условие вокруг самого middleware.

Лимит, зависящий от того, кто спрашивает

Колбэк получает запрос, поэтому лимит может отличаться:

php
RateLimiter::for('api', function ($request) {
    return $request->user()
        ? Limit::perMinute(1000)->by('user:' . $request->user()->id)
        : Limit::perMinute(60)->by($request->ip());
});

Несколько лимитов сразу

Верните массив — проверятся все, победит первый превышенный:

php
RateLimiter::for('sms', fn($request) => [
    Limit::perMinute(3)->by($request->ip()),
    Limit::perDay(20)->by($request->ip()),
]);

Это обычная форма для всего, что стоит денег за вызов: лимит на всплеск плюс суточный потолок.

Что видит вызывающий

Сверх лимита — 429 с текстом Too Many Attempts.: JSON-ом, если запрос ждёт JSON, и простым текстом иначе.

Заголовки есть у всех ответов, не только у отклонённых:

ЗаголовокСмысл
X-RateLimit-LimitПотолок
X-RateLimit-RemainingСколько осталось
Retry-AfterСекунд до сброса окна — только на 429
X-RateLimit-ResetМетка времени сброса — только на 429

Свой ответ — одним вызовом в цепочке:

php
Limit::perMinute(5)->response(fn($request, $retryAfter) =>
    response()->json(['error' => 'slow_down', 'retry_after' => $retryAfter], 429)
);

Заголовки лимита допишутся и к вашему ответу, так что забыть их нельзя.

Корзины

Ключ счётчика — throttle|область|кто.

  • У именованного лимитера область — это имя, поэтому один лимитер на пяти маршрутах считает их вместе. Для login это обычно то, что нужно; для несвязанных эндпоинтов обычно нет.
  • У встроенного лимита область — метод плюс путь, поэтому каждый маршрут считается отдельно.

Необъявленный лимитер пропускает запрос

throttle:опечатка пишет предупреждение в лог MODX и пропускает запрос. Он падает открытым — из тех соображений, что опечатка в имени middleware не должна класть форму, — но это же значит, что опечатка оставляет вас вовсе без защиты. После добавления лимитера загляните в лог.

Ручное использование

Счётчик доступен напрямую, для случаев, которые не про маршрут:

php
RateLimiter::tooManyAttempts($key, 5);   // превышено?
RateLimiter::hit($key, 60);              // засчитать попытку, окно 60 с
RateLimiter::remaining($key, 5);
RateLimiter::availableIn($key);          // секунд до сброса
RateLimiter::resetAttempts($key);

Счётчики живут в кэше MODX под pageblocks/ratelimit, поэтому очистка кэша сайта обнуляет их.

© PageBlocks 2019-present