Делаем мидлвар с троттлингом

Как сделать свой собственный мидлвар с троттлингом

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

В стандартной комплектации Laravel поставляется с мидлваром ThrottleRequests, который легко можно настроить на ограничение запросов с одного IP-адреса.

Например, приведенный ниже фрагмент в файле маршрутов ограничивает количество запросов, которые могут сделать пользователь или IP-адрес, до 60 в минуту:

Route::middleware('throttle:60,1')->group(function () {
    Route::get('/products', function () {
        //
    });
    Route::get('/locations', function () {
        //
    });
});

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

Route::middleware('throttle:rate_limit,60')->group(function () {
    Route::get('/products', function () {
        //
    });
    Route::get('/locations', function () {
        //
    });
});

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

Однако, что если вы хотите ограничить конкретный маршрут (или маршруты), но не использовать ни один из этих подходов?

В одном из последних наших проектов пользователи могли добавлять свои IoT устройства (Интернет вещей) и получать уникальный URL-адрес, который использовался как вебхук для отправки данных в нашу систему. Используя уникальный ключ из URL, мне нужно было регулировать создаваемые ими запросы.

На самом деле это довольно легко сделать, если знаешь как — просто нужно понять, как работает имеющийся мидлвар ThrottleRequests.

Если вы его откроете и посмотрите метод handle(), то увидите:

public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
    $key = $this->resolveRequestSignature($request);

    $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);

    if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
        throw $this->buildException($key, $maxAttempts);
    }

    $this->limiter->hit($key, $decayMinutes * 60);

    $response = $next($request);

    return $this->addHeaders(
        $response, $maxAttempts,
        $this->calculateRemainingAttempts($key, $maxAttempts)
    );
}

Первая строка метода — это то, что нас интересует:

$key = $this->resolveRequestSignature($request);

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

Метод в 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, если вы не меняли это) вашей модели User. В противном случае, в качестве ключа будет использоваться домен и IP-адрес запроса.

Зная это, нам просто нужно переопределить метод resolveRequestSignature() для того, чтобы получить наш собственный ключ для запроса:

1. Добавьте новый файл в app/Http/Middleware под названием CustomThrottleMiddleware (или как вы хотите называть свой мидлвар) или создайте через php artisan make:middleware CustomThrottleMiddleware

2. Замените содержимое на:

namespace App\Http\Middleware;

use Illuminate\Routing\Middleware\ThrottleRequests;

class CustomThrottleMiddleware extends ThrottleRequests
{
    protected function resolveRequestSignature($request)
    {
        //
    }
}

Мы создали новый мидлвар, которое расширяет ThrottleRequests. Он настроен на переопределение метода resolveRequestSignature(), чтобы мы могли установить наш собственный ключ.

Теперь просто нужно добавить свой код в resolveRequestSignature(), чтобы фактически вернуть ключ. Привожу несколько примеров:

protected function resolveRequestSignature($request)
{
    // Троттлинг по определенному заголовку
    return $request->header('API-Key');
}
protected function resolveRequestSignature($request)
{
    // Троттлинг по параметру запроса
    return $request->input('account_id');
}
protected function resolveRequestSignature($request)
{
    // Троттлинг по идентификатору сессии
    return $request->session();
}
protected function resolveRequestSignature($request)
{
    // Троттлинг по IP к конкретной модели в маршруте 
    // где 'product' является нашей моделью  
    // и этот маршрут настроен для использования 'product' с привязкой к маршруту
    return $request->route('product') . '|' . $request->ip();
}

В моем случае я выбрал троттлинг по ряду параметров. Уникальный URL-адрес, выдаваемый пользователю, был уникальным только для него, но одинаков для всех IoT устройств такого типа. Например, IoT устройства, которые мы поддерживали, имели типы A, B и C; если у вас было два типа A, то они использовали один и тот же уникальный URL. Наши причины этого не важны для статьи, поэтому я их опущу.

Каждому устройству было разрешено делать один запрос каждые 5 минут, однако, поскольку два устройства типа A были технически двумя отдельными устройствами, то нам нужно было сделать дополнительный способ их разделения.

Чтобы достичь этого, мы просто выясняли, с каким типом IoT-устройства мы работали, а затем разрешали токен соответствующим образом:

protected function resolveRequestSignature($request)
{
    $token = $request->route()->parameter('deviceByToken');

    $device = IotDevice::findByToken($token)->firstOrFail();

    if ($device->type === 'A')
    {
        return $token . '-' . $request->input('unique_parameter_for_type_a_per_device');
    } else if ($device->type === 'B')
    {
        return $token . '-' . $request->input('unique_parameter_for_type_b_per_device');
    } if ($device->type === 'C')
    {
        return $token . '-' . $request->input('unique_parameter_for_type_c_per_device');
    }

    return $token;
}

4. И последнее, но не менее важное: вам просто нужно зарегистрировать мидлвар для использования, обновив файл app/Http/Kernel.php следующим образом:

protected $routeMiddleware = [
    //...
    'customthrottle' => \App\Http\Middleware\CustomThrottleMiddleware::class,
];

Это позволит вам использовать ваш новый мидлвар следующим образом (измените 1 и 5 как вам нужно):

Route::middleware('customthrottle:1,5')->group(function () {
    Route::get('/products', function () {
        //
    });
    Route::get('/locations', function () {
        //
    });
});

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

Автор: James Bannister
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.