• Час читання ~4 хв
  • 11.08.2023

В кінці попередньої статті я згадав RateLimiter, а тому хотів би показати його використання на прикладі. Звичайно, ви можете знайти докладну документацію по цій темі на сайті Laravel, але я пропоную піти трохи далі. 

Перш за все, навіщо потрібно обмежувати доступ? Наприклад, ми хочемо:

  • Захистіть логін від грубої сили
  • захищати інші форми від відправки даних на сервер занадто часто (звичайні користувачі, звичайно, не стануть відправляти форму знову і знову, але зловмисники цілком можуть це зробити, причому програмно)
  • Обмежте кількість завантажень на сервер (особливо важливо, якщо ви дозволяєте користувачам завантажувати відео на ваш сайт)
  • обмежити надсилання електронних листів із сайту (наприклад, на сторінці контактної форми)
  • Тощо.

Отже, якщо в попередніх версіях ми використовували щось на кшталт:тепер при відкритті RouteServiceProvider побачимо:Відповідно, якщо припустити, що ми хочемо створити окремі ліміти для завантаження та надсилання електронних листів, і навіть додати власні відповіді, ми можемо додати необхідний код до методу configureRateLimiting() провайдера (далі app/Providers/RouteServiceProvider):, а потім застосувати його в маршрутних файлах:

Route::middleware('auth:api', 'throttle:60,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});

<?php
...
class RouteServiceProvider extends ServiceProvider
{
    ...
    public function boot()
    {
        $this->configureRateLimiting();
        ...
    }
    protected function configureRateLimiting()
    {
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
        });
    }
}

use Illuminate\Http\Response;
...
protected function configureRateLimiting()
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(10)->by(optional($request->user())->id ?: $request->ip());
    });
    RateLimiter::for('uploads', function (Request $request) {
        return Limit::perMinute(3)->by($request->user()?->id ?: $request->ip())
            ->response(function () {
                return response('Response for UPLOADS...', Response::HTTP_TOO_MANY_REQUESTS);
            });
    });
    RateLimiter::for('emails', function (Request $request) {
        return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip())
            ->response(function () {
                return response('Response for EMAILS...', Response::HTTP_TOO_MANY_REQUESTS);
            });
    });
}

Route::middleware(['throttle:uploads])->group(function () {
    ...
});
Route::middleware(['throttle:emails])->group(function () {
    ...
});

Це вже робочий варіант, але що робити, якщо проект не маленький, а у нас є, скажімо, 5 і більше різних лімітів? Навіть з урахуванням зайвих двох вище, дублювання вже дратує. Ось що ми зробимо:  

  • Щоб уникнути дублювання, винесемо створення ліміту в окремий метод, якому передамо загальні для всіх обмежень параметри, а саме: ім'я, максимальна кількість спроб, користувальницька відповідь і код відповіді HTTP (раптом чомусь потрібно дати не 429, а щось ще)
  • Граничні параметри будуть поміщені в конфігураційний файл. Тобто, якщо нам потрібен новий ліміт, все, що нам потрібно зробити, це додати його налаштування в конфігураційний файл
  • Звичайно, можна обійтися допоміжною функцією, але в цьому випадку доведеться працювати з асоціативним масивом і хардкодувати клавіші, що, особисто мені, теж не дуже подобається. Тому давайте створимо config()клас для налаштувань.

Почнемо з налаштування. У каталозі створіть файл і додайте до нього налаштування:Далі розберемося з класом. Оскільки клас перетворює масив в об'єкт, або іншими словами "закидає" масив в об'єкт, створіть Castsв каталозі піддиректорію, а в ній клас ThrottleConfig (тобто повний шлях до файлуapp/Casts/ThrottleConfig.phpconfigthrottle.phpapp). Код

<?php
use Illuminate\Http\Response;
return [
    'api' => [
        'max_attempts' => 100,
        'response' => null,
        'status' => null,
    ],
    'uploads' => [
        'max_attempts' => 3,
        'response' => 'Custom response for FORMS',
        'status' => Response::HTTP_TOO_MANY_REQUESTS,
    ],
    'emails' => [
        'max_attempts' => 5,
        'response' => 'Custom response for EMAILS',
        'status' => Response::HTTP_TOO_MANY_REQUESTS,
    ],
];

досить простий, і все ж, поясню:

<?php
namespace App\Casts;
class ThrottleConfig
{
    private string $for;
    private int $max_attempts;
    private ?int $status;
    private ?string $response;
    public function __construct(string $for, array $params)
    {
        $this->for = $for;
        collect($params)->each(fn ($value, $property) => $this->{$property} = $value);
    }
    public static function create(string $for, array $params): self
    {
        return new static($for, $params);
    }
    public function getFor(): string
    {
        return $this->for;
    }
    public function getMaxAttempts(): int
    {
        return $this->max_attempts;
    }
    public function getStatus(): int
    {
        return $this->status;
    }
    public function getResponse(): mixed
    {
        return $this->response;
    }
    public function hasResponse(): bool
    {
        return $this->response && $this->status;
    }
}

  • Конструктор бере ключ (наприклад, API, uploads, emails) і масив параметрів, які задає властивостям класу
  • Для хорошого потрібно додати сетерів - адже ніколи не знаєш, хто щось напише в конфігу, але заради простоти я опустю цю частину. Не думаю, що сетери викличуть труднощі
  • Що стосується добувачів, то замість них можна було б обійтися магічним методом __get(), при бажанні можна реалізувати
  • Метод статичного створення призначений для створення екземпляра
  • Оскільки обмеження можуть не містити користувацьких відповідей, давайте додамо метод, який перевіряє, чи є у нас відповідь.

Повертаємося до провайдера, і додаємо два приватних методу, перший бере налаштування з конфігурації:

private function limitingParameters(): array
{
    return config('throttle');
}

... а другий бере копію конфігурації і додає новий ліміт. При цьому пам'ятайте, що в залежності від параметрів, в деяких випадках може не бути призначеного для користувача варіанту відповіді: І останній штрих - модифікуємо метод (по суті, перегортаємо настройки в config/throttle.php циклі і викликаємо метод configureRateLimiting додавання ліміту):Повний код оновленого класу провайдера (phpdoc і скорочено видалений):

private function addLimit(ThrottleConfig $config): void
{
    RateLimiter::for($config->getFor(), function (Request $request) use ($config) {
        $limit = Limit::perMinute($config->getMaxAttempts())->by($request->user()?->id ?: $request->ip());
        return $config->hasResponse()
            ? $limit->response(fn () => response($config->getResponse(), $config->getStatus()))
            : $limit;
    });
}

protected function configureRateLimiting()
{
    collect($this->limitingParameters())->each(function ($params, $for) {
        $this->addLimit(ThrottleConfig::create($for, $params));
    });
}
<?php
namespace App\Providers;
use App\Casts\ThrottleConfig;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
    public const HOME = '/home';
    public function boot()
    {
        $this->configureRateLimiting();
        $this->routes(function () {
            Route::prefix('api')
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));
            Route::middleware('web')
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));
        });
    }
    protected function configureRateLimiting()
    {
        collect($this->limitingParameters())->each(function ($params, $for) {
            $this->addLimit(ThrottleConfig::create($for, $params));
        });
    }
    private function addLimit(ThrottleConfig $config): void
    {
        RateLimiter::for($config->getFor(), function (Request $request) use ($config) {
            $limit = Limit::perMinute($config->getMaxAttempts())->by($request->user()?->id ?: $request->ip());
            return $config->hasResponse()
                ? $limit->response(fn () => response($config->getResponse(), $config->getStatus()))
                : $limit;
        });
    }
    private function limitingParameters(): array
    {
        return config('throttle');
    }
}

На цьому на сьогодні все. Успіхів!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297