Установка лимита запросов — один из вариантов защиты приложения. Ведь согласитесь, обычный пользователь врятли сможет генерировать сотню запросов к приложению в минуту. В Yii такая защита имеется из коробки и основана на алгоритме Leaky bucket. Главное знать о ее существовании и использовать по назначению. Как настроить лимит запросов к приложению в Yii2 и рассмотрим в этой статье.
Для начала оговорюсь, лимитирование запросов (RateLimiter) — это фильтр, поэтому его можно использовать в любом (не только REST) контроллере или модуле, подключив в виде поведения.
public function behaviors() { return [ 'access' => [ ... ], 'rateLimiter' => [ 'class' => RateLimiter::className(), ], ... ]; }
Как и для любого фильтра можно использовать параметры only и except, чтобы лимит запросов работал только на некоторые из action.
Подключили фильтр, теперь нужно добавить в модель User имплементацию интерфейса RateLimitInterface
class User extends \yii\db\ActiveRecord implements RateLimitInterface, IdentityInterface { ... public function getRateLimit($request, $action) { return [100, 60]; //не более 100 запросов в течении 60 секунд } public function loadAllowance($request, $action) { ... //$count - считаем сколько уже запросов совершил юзер, например по записям в некой таблице логов return [100-$count, time()]; } public function saveAllowance($request, $action, $allowance, $timestamp) { ... //записываем результат запроса, например в некоторую таблицу логов } } ... }
Иногда нужно ограничить доступ к тем страницам, доступ к которым есть у не залогиненного юзера. Как же быть в этом случаи? В принципе все те-же 3 метода, но нужно указать класс, который имплементирует RateLimitInterface, т.е экземпляр класса, через который фильтр получит доступ к методам getRateLimit, loadAllowance и saveAllowance. В случаи с залогиненным юзером, экземпляр класса User получается с помощью метода Yii::$app->user->getIdentity(false). Нам же нужно явно передать экземпляр класса.
public function behaviors() { return [ 'access' => [ ... ], 'rateLimiter' => [ 'class' => RateLimiter::className(), 'user' => new IpLimiter() ], ... ]; }
Класс IpLimiter в моем примере — это мой класс, имлементирующий RateLimitInterface, т.е содержащий методы getRateLimit, loadAllowance и saveAllowance. Реализация хранения данных о совершенных запросах, логика подсчета запросов и прочее — зависит от нужд приложения.
Что произойдет, если пользователь исчерпал лимит запросов: приложение бросить исключение вида TooManyRequestsHttpException (код 429).
Если вам интересно, как же работает данный фильтр, то загляните в метод checkRateLimit() класса yii\filters\RateLimiter. Надеюсь удалось прояснить вопрос. Если что-то осталось не понятным — спрашивайте в комментариях.
1) Как протестировать нагрузку
2) Как правильно реагировать на нее
1) Отправлять большое кол-во запросов. Во время разработки, чтобы проверить что лимит работает правильно, я ставлю маленькое ограничение, которое легко достигается вручную: не более 2 запросов в 5 минут к примеру. А на продакшене ставлю уже нормальные значения. Уточню — RateLimit не оптимизирует нагрузку, а только защищает от перенагрузки.
2) Реагировать отдельно на нее не нужно. При достижении лимита — запросы просто не будут проходить (приложение не будет выполнять лишную работу в action). Т.е это должно защитить от атак типа DoS.
Спасибо за статьи, очень помогают в работе! Вот сейчас как раз понадобился IpLimiter, есть на что опереться.
Спасибо вам за отзыв 🙂
добрый день
подскажите пожалуйста, каким образом работает РейтЛимитер?
может уже есть реализованый пример, как именно работает, и как именно выбрасывается исключение?
например, юзер логинится 10 раз подряд
есть табличка входов польователя, в $count = Ip::find([‘ip’ => Yii::$app->request->userIP])->count();, например так
далее return [5 — $count, time()];
что далее?
где проверять, и где отдавать заголовки что превышен лимит запросов?
буду благодарен если уже есть реализовано всё
Вручную ничего проверять не нужно, в этом и суть. Вы просто указываете в контроллере фильтр
И все! Фильтр срабатывает до action и проверяет лимит по классу юзера (методы описаны в статье). Если лимит привышен Yii Framework бросит exception
Грубо говоря: если веб страница то будет сообщение «Rate limit exceeded.», так же как выглядит любой другой, к примеру, Fobiden Exception. Если Rest то ответ сервера будет с кодом 429 и сообщением «Rate limit exceeded.»
Сообщение можно поменять
А экспепшен можно отловить и обработать. Можно самому, а можно взять мой компонент и описать только поведение в случаи привышения лимита.
Я так понял, что, если я установил максимум 100 запросов за 60 секунд, то каждый запрос сбрасывает таймер. То есть, после того, как клиент превысил 100 запросов в минуту, в течении следующей минуты, ему не удастся сделать ни одного запроса.
С точки зрения хранения данных и TTL это выглядит оптимально, но это очевидно клиенту.
Если первые 99 запросов он совершил в первую секунду, а последний запрос, на 59 секунде, то, скажем, в на 1:40 он не сможет совершить запрос, хотя, в период с 0:40 по 1:40 был совершен всего 1 запрос.
Вопрос. Действительно ли такая логика зашита в коробку, или это ошибка моей реализации?
Ни какая логика не зашита в коробку, все зависит от вашей реализации. В моем примере считается общее кол-во запросов в минуту. Вы можете смотреть еще и на время между запросами.
Тогда кол-во будет считаться только если запросы идут чаще чем раз в 10 секунд.