/ laravel

Как в Laravel задать лимимы запросов к API

В этой статье я расскажу, как в Laravel реализовать ограничение доступа к API по количеству сделанных запросов. Здесь я покажу всё на примере встроенных инструментов во фреймворк (laravel api rate limit, используя api throttle limit): как с ними работать, настраивать, переопределять.

Что такое ограничение запросов?

Ограничения запросов - это контроль количества запросов, которые пользователь может сделать в течении конкретного времени. Ограничения могут быть применены к портам, IP-адресам или конкретным маршрутам роутера.

Ограничение количества запросов может быть реализовано на уровне ПО системы, через файрвол. Для того, чтобы задать лимиты обращений к сайту через файрвол, нужно выполнить:
iptables -I INPUT -p tcp --dport 80 -i eth0 -m state --state NEW -m recent --set
iptables -I INPUT -p tcp --dport 80 -i eth0 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 -j DROP

Это ограничит запросы к 80 порту, в количестве 80 запросов в минуту. Это рабочий вариант, но он недостаточно гибкий. В этом случае не получится тонкой настройки количества запросов к конкретным маршрутам API, или настройки индивидуальных лимитов для каждого из пользователей. Но, к счастью, в Laravel, начиная с версии 5.2, реализован механизм в лице Throttling Middleware, который позволяет навесить лимиты достаточно удобно и гибко.

Указание лимитов запросов в Laravel

Для начала нужно создать API, на примере которого мы будем тестировать ограничения запросов. Чтобы сильно не удаляться от основной идеи статьи, реализуем несколько простых методов API, которые не будут взаимодействовать с БД, а будут работать только с кешем:

<?php

use Illuminate\Http\Request;
use Carbon\Carbon;

Route::group(['prefix' => 'v1'], function() {
    Route::get('/', function() {
        return response()->json(['data' => Cache::get('items', [])]);
    });

    Route::post('/', function(Request $request) {
        $validator = Validator::make($request->all(), [
            'text' => 'required|max:255',
        ]);

        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 200);
        }

        $value = Cache::get('items', []);
        $value[] = $request->text;

        Cache::put('items', $value, Carbon::now()->addDays(30));

        return response()->json(['message' => "Added new value: {$request->text}"]);
    });
});

Теперь, воспользовавшись любым HTTP-клиентом, добавим новую запись, отправив POST-запрос с параметром text по адресу /api/v1:
post_store

После чего можем просмотреть все добавленные записи, отправив GET-запрос по тому же адресу: get_list

С результата видно, что запись "Some new text" была добавлена. Сейчас, повторим этот запрос 10 раз с минимальным интервалом, чтобы записей было больше.

Сделав это, можно увидеть, что нет никаких ограничений по добавлению записей. Иногда это хорошо, иногда плохо, всё зависим от бизнес-задач. Но, раз мы рассматривает эту тему, значит, текущая бизнес-задача требует конкретных ограничений обращений к API.

Теперь, продвинемся дальше, и добавим наконец-таки ограничения на количество запросов в минуту. Для этого, напишем такой код, обернув маршруты API во встроенный Middleware Throttle.

Route::group(['prefix' => 'v1', 'middleware' => 'throttle:3,10'], function() {
    ...
});

Этот код позволяет пользователю с одного IP-адреса выполнять по 3 запроса в течении каждых 10 минут. И в следующий раз, когда пользователь сделает запрос к вашему API с одного IP-адреса, больше указанного лимита, он получит ответ от сервера:

...
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 0
< Retry-After: 55
...

Со статусом 429 Too many requests

Ключом к надёжности вашего API и удобностью использования является то, как вы найдёте правильный баланс в ограничениях на запросы. К примеру, если 3 запроса в каждые 10 минут для вашего приложения - слишком жестко, то вы можете поменять на 3 запроса в минуту, и т.д. методом подбора и тестирования. По умолчанию, все запросы в Throttle Middleware применимы к 1 минуте, потому, второй аргумент можно не указывать, если вы указываете ограничения относительно минуты: 'middleware' => 'throttle:300', это ограничивает до 300 запросов в минуту.

Как всё устроено в Throttle Middleware?

Пример выше по которому мы реализовали ограничения возможен благодаря классу Illuminate\Routing\Middleware\ThrottleRequests, который и проверяет количество сделанных пользователем запросов, сравнивает их с лимитами и принимает решение, какой ответ вернуть пользователю.

Сейчас метод этого класса по учёту лимитов выглядит так:

protected function resolveRequestSignature($request)
{
    if ($user = $request->user()) {
        return sha1($user->getAuthIdentifier());
    }

    if ($route = $request->route()) {
        return sha1($route->getDomain().'|'.$request->ip());
    }

    throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
}

В этом коде, если пользователь авторизован, то лимиты сверяются по его идентификатору id, если неавторизован, то по его IP-адресу. Но, если мы хотим сделать проверку лимитов по чему-то другому, не по IP, или идентификатору, то нужно унаследоваться от класса ThrottleRequests и переопределить метод resolveRequestSignature (чистой воды ООП):

protected function resolveRequestSignature($request)
{
    if ($route = $request->route()) {
            return sha1($route->getDomain().'|'.$request->input('api_token'));
        }

        throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
}

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

return sha1($route->getDomain().'|'.$request->session()->getId());
// или
return sha1($route->getDomain().'|'.$request->header('api_token'));
// или можете ограничивать запросы по конкретному параметру в запросе
return sha1($route->getDomain().'|'.$request->get('user_id'));

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

Учтите, что, когда вы сделаете свою унаследованную реализацию ThrottleRequests, вам потребуется в файле app/Http/Kernel.php подменить ThrottleRequests своим классом custom

Динамический параметр лимитов

Начиная с Laravel 5.6, вы можете указать индивидуальные лимиты для пользователей, как параметр модели User, используя функцию динамического ограничения лимитов запросов.

Фреймворк очень упрощает нам процесс добавления индивидуальных лимитов для API Laravel. Рассмотрим пример, используемый ранее, только добавив обязательную пользовательскую аутентификацию:

Route::group(['prefix' => 'v1', 'middleware' => ['auth:api', 'throttle:60']], function() {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
});

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

Потому, был придуман следующий вариант:

'middleware' => ['auth:api', 'throttle:rate_limit,1']

И в этом примере, мы так же используем Throttle Middleware, но вместо жёстко заданного числа, указываем строковое значение rate_limit, указывающее на имя поля в таблице User. Зная это, разработчик может легко определить в базе данных количество запросов для конкретного пользователя, просто задав ему значение в базе данных.

Включение/отключение ограничений

Ограничение количества запросов к API по-умолчанию действует для всех API-методов. Для того, чтобы полностью снять ограничения по запросам, нужно в файле .env добавить параметр API_RATE_LIMIT_ENABLED, указав ему значение false.

Или, удалите в файле app/Http/Kernel.php строку, задающую ограничения throttle:60,1: middleawre_delete

Резюме

Ограничения для API - это важная функция, реализовать которую очень часто ставят перед разработчиков, особенно на платформе с подписками на индивидуальные тарифы. И благодаря Laravel, это функцию можно реализовать всего за несколько действий, сохраняя при этом хорошую архитектуру кода и его читаемость. Потому, изучив эту статью, вы должны знать, как сделать лимиты обращений к API для конкретного пользователя, или группы пользователей.
В этой статье, я показал, как реализовать ограничения запросов к API с помощью встроенных средств фреймворка, как задать индивидуальные лимиты для конкретного пользователя, а так же, как сделать это используя системный файрвол.
Помимо этого, было рассмотрена структура Throttling Middleware, что позволит вам кастомизировать "посредника" как угодно, под ваши нужды.