Лимитирование запросов (RateLimiter) к приложению в Yii2

22 сентября, 2015
Метки: , ,

Установка лимита запросов — один из вариантов защиты приложения. Ведь согласитесь, обычный пользователь врятли сможет генерировать сотню запросов к приложению в минуту. В Yii такая защита имеется из коробки и основана на алгоритме Leaky bucket. Главное знать о ее существовании и использовать по назначению. Как настроить лимит запросов к приложению в Yii2 и рассмотрим в этой статье.
Для начала оговорюсь, лимитирование запросов (RateLimiter) — это фильтр, поэтому его можно использовать в любом (не только REST) контроллере или модуле, подключив в виде поведения.

 public function behaviors()
    {
        return [
            'access' => [
              ...
            ],
            'rateLimiter' => [
                'class' => RateLimiter::className(),
            ],
            ...
        ];
    }

Как и для любого фильтра можно использовать параметры only и except, чтобы лимит запросов работал только на некоторые из action.

Не больше N-запросов в течении T-времени от одного юзера

Подключили фильтр, теперь нужно добавить в модель 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 метода:

  • getRateLimit возвращает общее число разрешенных запросов в некое время
  • loadAllowance возвращает количество оставшихся запросов и время ответа
  • saveAllowance сохраняет текущий запрос, чтобы потом его можно было посчитать в loadAllowance

Не больше N-запросов в течении T-времени с одного IP

Иногда нужно ограничить доступ к тем страницам, доступ к которым есть у не залогиненного юзера. Как же быть в этом случаи? В принципе все те-же 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. Надеюсь удалось прояснить вопрос. Если что-то осталось не понятным — спрашивайте в комментариях.


Метки: , ,

Оставить комментарий

8 комментариев »

    Roman

    1) Как протестировать нагрузку
    2) Как правильно реагировать на нее

      Developer

      1) Отправлять большое кол-во запросов. Во время разработки, чтобы проверить что лимит работает правильно, я ставлю маленькое ограничение, которое легко достигается вручную: не более 2 запросов в 5 минут к примеру. А на продакшене ставлю уже нормальные значения. Уточню — RateLimit не оптимизирует нагрузку, а только защищает от перенагрузки.
      2) Реагировать отдельно на нее не нужно. При достижении лимита — запросы просто не будут проходить (приложение не будет выполнять лишную работу в action). Т.е это должно защитить от атак типа DoS.


    Михаил

    Спасибо за статьи, очень помогают в работе! Вот сейчас как раз понадобился IpLimiter, есть на что опереться.

      Developer

      Спасибо вам за отзыв 🙂


    devo

    добрый день
    подскажите пожалуйста, каким образом работает РейтЛимитер?
    может уже есть реализованый пример, как именно работает, и как именно выбрасывается исключение?
    например, юзер логинится 10 раз подряд
    есть табличка входов польователя, в $count = Ip::find([‘ip’ => Yii::$app->request->userIP])->count();, например так
    далее return [5 — $count, time()];

    что далее?
    где проверять, и где отдавать заголовки что превышен лимит запросов?
    буду благодарен если уже есть реализовано всё

      Developer

      Вручную ничего проверять не нужно, в этом и суть. Вы просто указываете в контроллере фильтр

      public function behaviors()
        {
            return [
                 ...
                'rateLimiter' => [
                    'class' => \yii\filters\RateLimiter::className(),
                ],
            ];
        }
      

      И все! Фильтр срабатывает до action и проверяет лимит по классу юзера (методы описаны в статье). Если лимит привышен Yii Framework бросит exception

      throw new TooManyRequestsHttpException($this->errorMessage);
      

      Грубо говоря: если веб страница то будет сообщение «Rate limit exceeded.», так же как выглядит любой другой, к примеру, Fobiden Exception. Если Rest то ответ сервера будет с кодом 429 и сообщением «Rate limit exceeded.»
      Сообщение можно поменять

           'rateLimiter' => [
                    'class' => \yii\filters\RateLimiter::className(),
                    'errorMessage' => 'Все попытки входа исчерпаны'
                ],
      

      А экспепшен можно отловить и обработать. Можно самому, а можно взять мой компонент и описать только поведение в случаи привышения лимита.


    Илья

    Я так понял, что, если я установил максимум 100 запросов за 60 секунд, то каждый запрос сбрасывает таймер. То есть, после того, как клиент превысил 100 запросов в минуту, в течении следующей минуты, ему не удастся сделать ни одного запроса.
    С точки зрения хранения данных и TTL это выглядит оптимально, но это очевидно клиенту.
    Если первые 99 запросов он совершил в первую секунду, а последний запрос, на 59 секунде, то, скажем, в на 1:40 он не сможет совершить запрос, хотя, в период с 0:40 по 1:40 был совершен всего 1 запрос.
    Вопрос. Действительно ли такая логика зашита в коробку, или это ошибка моей реализации?

      Developer

      Ни какая логика не зашита в коробку, все зависит от вашей реализации. В моем примере считается общее кол-во запросов в минуту. Вы можете смотреть еще и на время между запросами.

      public function loadAllowance($request, $action)
       {
         ...
         //$count - считаем сколько уже запросов совершил юзер, например по записям в некой таблице логов
         //$secondsFromLastRequest - секунд с прошлого запроса
         if($secondsFromLastRequest < 10) {
         return [100-$count, time()];
         } else return [100, time()];
       }
      

      Тогда кол-во будет считаться только если запросы идут чаще чем раз в 10 секунд.


Оставить комментарий:









Копирование материалов разрешено при наличии активной ссылки на источник