• Reading time ~ 6 min
  • 11.08.2023

At the end of the previous article , I mentioned RateLimiter, and therefore I would like to show its use with an example. Of course, you can find detailed documentation on this topic on the Laravel website, but I suggest going a little further. 

First of all, why do you need to restrict access? For example, we want to:

  • Protect login from brute force
  • protect other forms from sending data to the server too often (ordinary users, of course, will not send the form over and over again, but attackers may well do this, and programmatically)
  • limit the number of uploads to the server (especially important if you allow users to upload videos to your site)
  • restrict sending emails from the site (for example, on the contact form page)
  • etc.

So, if in previous versions, we used something like:now when we open RouteServiceProvider we will see:Accordingly, if we assume that we want to create separate limits for downloading and sending emails, and even add custom responses, we can add the necessary code to the provider's method configureRateLimiting() (hereinafter referred to app/Providers/RouteServiceProvideras ):and then apply it in route files:

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

This is already a working option, but what if the project is not small, and we have, say, 5 or more different limits? Even with the extra two above, duplication is already annoying. Here's what we'll do:  

  • To avoid duplication, let's put the creation of the limit in a separate method, to which we will pass the parameters common to all restrictions, namely: name, maximum number of attempts, custom response and HTTP response code (suddenly, for some reason, you need to give not 429, but something else)
  • The limit parameters will be placed in the configuration file. That is, if we need a new limit, all we need to do is add its settings to the config file
  • Of course, we can get by with a helper function, but in this case we will have to work with an associative array and hardcode the keys, which, personally, I don't really like either. Therefore, let's create a config()class for settings.

Let's start with the configuration. In the directory, create a file and add settings to it:Next, let's deal with the class. Since the class converts an array to an object, or in other words "casts" an array to an object, create a Castssubdirectory in the directory, and in it a class ThrottleConfig (i.e. the full path to the app config fileapp/Casts/ThrottleConfig.phpthrottle.php). The code is

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

quite simple, and yet, I will explain:

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

  • The constructor takes a key (for example, API, uploads, emails) and an array of parameters that it sets to the properties of the class
  • For good, you need to add setters - after all, you never know who will write something in the config, but for the sake of simplicity, I will omit this part. I don't think setters will cause difficulties
  • As for the getters, instead of them it would be possible to do with a magical method __get(), if you wish, you can implement
  • The static create method is intended for creating an instance
  • Since the limits may not contain custom responses, let's add a method that checks if we have an answer.

We return to the provider, and add two private methods, the first takes the settings from the config:

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

... and the second one takes a copy of the config and adds a new limit. At the same time, remember that depending on the parameters, in config/throttle.php some cases there may not be a custom version of the response: And the final touch - we modify the method (in fact, we scroll through the settings in the loop and call the method configureRateLimiting for adding a limit):The full code of the updated provider class (phpdoc and removed for short):

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

That's all for today. Good luck!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

ABOUT

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

About author CrazyBoy49z
WORK EXPERIENCE
Contact
Ukraine, Lutsk
+380979856297