Ограничение частоты
Ограничение по маршруту, через middleware throttle.
Route::post('form/submit', 'FormController@handle')->middleware('throttle');Три способа задать лимит
| Запись | Значит |
|---|---|
throttle | 60 запросов в минуту |
throttle:20 | 20 в минуту |
throttle:100,5 | 100 за 5 минут |
throttle:login | именованный лимитер login |
Встроенных форм хватает в большинстве случаев. Именованный нужен, когда лимит зависит от запроса.
Именованные лимитеры
Объявляется один раз, дальше используется по имени:
use Boshnik\PageBlocks\Routing\Limit;
use Boshnik\PageBlocks\Support\RateLimiter;
RateLimiter::for('login', function ($request) {
return Limit::perMinute(5)->by($request->ip());
});Route::post('login', 'AuthController@login')->middleware('throttle:login');Как собрать лимит
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.
Лимит, зависящий от того, кто спрашивает
Колбэк получает запрос, поэтому лимит может отличаться:
RateLimiter::for('api', function ($request) {
return $request->user()
? Limit::perMinute(1000)->by('user:' . $request->user()->id)
: Limit::perMinute(60)->by($request->ip());
});Несколько лимитов сразу
Верните массив — проверятся все, победит первый превышенный:
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 |
Свой ответ — одним вызовом в цепочке:
Limit::perMinute(5)->response(fn($request, $retryAfter) =>
response()->json(['error' => 'slow_down', 'retry_after' => $retryAfter], 429)
);Заголовки лимита допишутся и к вашему ответу, так что забыть их нельзя.
Корзины
Ключ счётчика — throttle|область|кто.
- У именованного лимитера область — это имя, поэтому один лимитер на пяти маршрутах считает их вместе. Для
loginэто обычно то, что нужно; для несвязанных эндпоинтов обычно нет. - У встроенного лимита область — метод плюс путь, поэтому каждый маршрут считается отдельно.
Необъявленный лимитер пропускает запрос
throttle:опечатка пишет предупреждение в лог MODX и пропускает запрос. Он падает открытым — из тех соображений, что опечатка в имени middleware не должна класть форму, — но это же значит, что опечатка оставляет вас вовсе без защиты. После добавления лимитера загляните в лог.
Ручное использование
Счётчик доступен напрямую, для случаев, которые не про маршрут:
RateLimiter::tooManyAttempts($key, 5); // превышено?
RateLimiter::hit($key, 60); // засчитать попытку, окно 60 с
RateLimiter::remaining($key, 5);
RateLimiter::availableIn($key); // секунд до сброса
RateLimiter::resetAttempts($key);Счётчики живут в кэше MODX под pageblocks/ratelimit, поэтому очистка кэша сайта обнуляет их.