• Czas czytania ~6 min
  • 11.08.2023

Na końcu poprzedniego artykułu wspomniałem o RateLimiter, dlatego chciałbym pokazać jego zastosowanie na przykładzie. Oczywiście szczegółową dokumentację na ten temat można znaleźć na stronie Laravel, ale proponuję pójść nieco dalej. 

Po pierwsze, dlaczego musisz ograniczyć dostęp? Na przykład chcemy:

  • Chroń logowanie przed brutalną siłą
  • chronić inne formularze przed zbyt częstym wysyłaniem danych na serwer (zwykli użytkownicy oczywiście nie będą wysyłać formularza w kółko, ale atakujący mogą to zrobić i programowo)
  • Ogranicz liczbę przesyłanych filmów na serwer (szczególnie ważne, jeśli zezwalasz użytkownikom na przesyłanie filmów do Twojej witryny)
  • Ogranicz wysyłanie wiadomości e-mail z witryny (na przykład na stronie formularza kontaktowego)
  • itd.

Tak więc, jeśli w poprzednich wersjach używaliśmy czegoś takiego:teraz po otwarciu RouteServiceProvider zobaczymy:W związku z tym, jeśli założymy, że chcemy stworzyć oddzielne limity pobierania i wysyłania wiadomości e-mail, a nawet dodawać niestandardowe odpowiedzi, możemy dodać niezbędny kod do metody configureRateLimiting() dostawcy (zwanej app/Providers/RouteServiceProviderdalej ):, a następnie zastosować go w plikach trasy:

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 () {
    ...
});

Jest to już działająca opcja, ale co, jeśli projekt nie jest mały, a my mamy, powiedzmy, 5 lub więcej różnych limitów? Nawet z dodatkowymi dwoma powyżej, duplikacja jest już denerwująca. Oto, co zrobimy:  

  • Aby uniknąć duplikacji, umieśćmy tworzenie limitu w osobnej metodzie, do której przekażemy parametry wspólne dla wszystkich ograniczeń, a mianowicie: nazwę, maksymalną liczbę prób, niestandardową odpowiedź i kod odpowiedzi HTTP (nagle, z jakiegoś powodu, musisz podać nie 429, ale coś innego)
  • Parametry limitu zostaną umieszczone w pliku konfiguracyjnym. Oznacza to, że jeśli potrzebujemy nowego limitu, wszystko, co musimy zrobić, to dodać jego ustawienia do pliku konfiguracyjnego
  • Oczywiście możemy sobie poradzić z funkcją pomocniczą, ale w tym przypadku będziemy musieli pracować z tablicą asocjacyjną i zakodować klucze na stałe, co osobiście też mi się nie podoba. Dlatego utwórzmy klasę config()dla ustawień.

Zacznijmy od konfiguracji. W katalogu utwórz plik i dodaj do niego ustawienia:Następnie zajmijmy się klasą. Ponieważ klasa konwertuje tablicę na obiekt, lub innymi słowy "rzutuje" tablicę na obiekt, utwórz Castspodkatalog w katalogu, a w nim klasę ThrottleConfig (tj. pełną ścieżkę do app config plikuapp/Casts/ThrottleConfig.phpthrottle.php). Kod jest

<?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,
    ],
];

dość prosty, a jednak wyjaśnię:

<?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;
    }
}

  • Konstruktor pobiera klucz (na przykład API, uploads, emails) i tablicę parametrów, które ustawia do właściwości klasy
  • Na dobre trzeba dodać setery - w końcu nigdy nie wiadomo, kto coś napisze w configu, ale dla uproszczenia pominę tę część. Nie sądzę, że setery będą sprawiać trudności
  • Jeśli chodzi o zdobywców, zamiast nich można by zrobić magiczną metodę __get(), jeśli chcesz, możesz wdrożyć
  • Metoda tworzenia statycznego jest przeznaczona do tworzenia instancji
  • Ponieważ limity mogą nie zawierać niestandardowych odpowiedzi, dodajmy metodę, która sprawdza, czy mamy odpowiedź.

Wracamy do dostawcy i dodajemy dwie prywatne metody, pierwsza pobiera ustawienia z konfiguracji:

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

... a drugi pobiera kopię konfiguracji i dodaje nowy limit. Jednocześnie pamiętaj, że w zależności od parametrów, w niektórych przypadkach może nie być niestandardowej wersji odpowiedzi: I ostatni akcent - modyfikujemy metodę (w rzeczywistości przewijamy ustawienia w pętli i wywołujemy metodę dodawania limitu):Pełny kod zaktualizowanej klasy dostawcy (phpdoc i usunięty w config/throttle.php skrócie):

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');
    }
}

To wszystko na dziś. Powodzenia!configureRateLimiting

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297