В кінці попередньої статті я згадав 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.php
config
throttle.php
app
). Код
<?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');
}
}
На цьому на сьогодні все. Успіхів!