When providing APIS to the public network for external access to data, in order to avoid malicious attacks in addition to token authentication, it is better to add request frequency limit to the API. Since 5.2 in Laravel, Throttle, the component of the framework, supports access frequency limit and provides a Throttle middleware for us to use. However, Throttle middleware returns an HTML response telling you that your request has been overclocked when you access the API at a limited frequency. In an application, you would prefer to return an API response rather than an HTML response. So in this article, we will provide a custom middleware to replace the default Throttle middleware to implement the custom response content.

Overview of access frequency limits

Frequency limits are often used in apis to limit the frequency of requests from individual requesters to a particular API. For example, if you set the frequency limit to 1000 Attempts per minute, the server will return 429: Too Many Attempts if this limit is exceeded in one minute. The response.

Generally, a well-coded application that implements the frequency Limit returns three response headers: X-Ratelimit-Limit, X-Ratelimit-Remaining, and Retry-After (the Retry-After header returns only After the frequency Limit is reached). X-ratelimit-limit indicates the maximum number of requests allowed within a specified period of time. X-ratelimit-remaining indicates the number of requests Remaining within a specified period of time. Retry-after indicates the time (s) before the next Retry.

Note: Each application selects its own frequency limit time span. The Laravel application’s frequency limit time span is one minute, so the frequency limit is limited to the number of accesses per minute.

Using Throttle middleware

Let’s take a look at how this middleware is used. First, we define a route to which we add throttle, which is limited to 60 attempts per minute by default, and will be banned if the number of attempts reaches 60 in a minute:

Route::group(['prefix'= >'api'.'middleware'= >'throttle'].function(){
    Route::get('users'.function() {return \App\User::all();
    });
});
Copy the code

When you access routing/API/Users you will see the following information in the response header:

X-RateLimit-Limit: 60 X-RateLimit-Remaining: 58

If the request is overclocked, the response header returns retry-after:

Retry-After: 58 X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0

The above information indicates that the page or API access will be restored after 58 seconds.

Define frequency and retry wait time

The default frequency is 60, which can be specified by the first parameter of throttle’s middleware. The default retry wait time is one minute, which can be specified by the second parameter of Throttle’s middleware.

Route::group(['prefix'= >'api'.'middleware'= >'throttle:5'].function(){
    Route::get('users'.function() {return\App\User::all(); }); }); Route::group(['prefix'= >'api'.'middleware'= >'throttle: 5, 10'].function(){
    Route::get('users'.function() {return\App\User::all(); }); }); // Frequency upper limit 5, retry wait time 10 minutesCopy the code

### Custom Throttle middleware that returns API responses. In addition to the response headers that are returned when the number of requests reaches the upper limit, the Throttle Throttle returns an HTML page that tells us Too Many Attempts. We would prefer to get a JSON response when calling the API, so we provide a custom middleware to define the response information instead of the default Throttle middleware.

First create a ThrottleRequests middleware: PHP Artisan Make: Middleware ThrottleRequests.

To copy the code below to Http/Middlewares/app/ThrottleReuqests file:

<? php namespace App\Http\Middleware; use Closure; use Illuminate\Cache\RateLimiter; use Symfony\Component\HttpFoundation\Response; class ThrottleRequests { /** * The rate limiter instance. * * @var \Illuminate\Cache\RateLimiter */ protected$limiter;

    /**
     * Create a new request throttler.
     *
     * @param  \Illuminate\Cache\RateLimiter $limiter
     */
    public function __construct(RateLimiter $limiter)
    {
        $this->limiter = $limiter;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @param  int $maxAttempts
     * @param  int $decayMinutes
     * @return mixed
     */
    public function handle($request, Closure $next.$maxAttempts = 60, $decayMinutes = 1)
    {
        $key = $this->resolveRequestSignature($request);

        if ($this->limiter->tooManyAttempts($key.$maxAttempts.$decayMinutes)) {
            return $this->buildResponse($key.$maxAttempts);
        }

        $this->limiter->hit($key.$decayMinutes);

        $response = $next($request);

        return $this->addHeaders(
            $response.$maxAttempts.$this->calculateRemainingAttempts($key.$maxAttempts)); } /** * Resolve request signature. * * @param \Illuminate\Http\Request$request
     * @return string
     */
    protected function resolveRequestSignature($request)
    {
        return $request->fingerprint();
    }

    /**
     * Create a 'too many attempts' response.
     *
     * @param  string $key
     * @param  int $maxAttempts
     * @return \Illuminate\Http\Response
     */
    protected function buildResponse($key.$maxAttempts)
    {
        $message = json_encode([
            'error'= > ['message'= >'Too many attempts, please slow down the request.' //may comes from lang file
            ],
            'status_code' => 4029 //your custom code
        ]);

        $response = new Response($message, 429);

        $retryAfter = $this->limiter->availableIn($key);

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

    /**
     * Add the limit header information to the given response.
     *
     * @param  \Symfony\Component\HttpFoundation\Response $response
     * @param  int $maxAttempts
     * @param  int $remainingAttempts
     * @param  int|null $retryAfter
     * @return \Illuminate\Http\Response
     */
    protected function addHeaders(Response $response.$maxAttempts.$remainingAttempts.$retryAfter = null)
    {
        $headers = [
            'X-RateLimit-Limit'= >$maxAttempts.'X-RateLimit-Remaining'= >$remainingAttempts,];if(! is_null($retryAfter)) {
            $headers['Retry-After'] = $retryAfter;
            $headers['Content-Type'] = 'application/json';
        }

        $response->headers->add($headers);

        return $response;
    }

    /**
     * Calculate the number of remaining attempts.
     *
     * @param  string $key
     * @param  int $maxAttempts
     * @param  int|null $retryAfter
     * @return int
     */
    protected function calculateRemainingAttempts($key.$maxAttempts.$retryAfter = null)
    {
        if(! is_null($retryAfter)) {
            return 0;
        }

        return $this->limiter->retriesLeft($key.$maxAttempts); }}Copy the code

App /Http/ kernel.php

'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
Copy the code

Replace:

'throttle' => \App\Http\Middleware\ThrottleRequests::class,
Copy the code

And we’re done.

Throttle information storage

And finally, Throttle and all of these frequency data are stored in cache, The default cache driver for Laravel is file, which means you are storing all your throttle data in the framework cache file. If you change your cache driver to Redis, you are storing all your throttle data in the framework cache file. Throttle will use the signature of the request object (hashing the HTTP request method, domain name, URI, and client IP) as a cache key to record the number of requests made by the client.