Quer limitar o número de requisições na sua api?
Tags: beginners, laravel, php, ratelimit
Published: 2022-07-19 21:19:21


Neste artigo vou mostrar a implementação de um middleware que controla a quantidade de requisições em uma rota ou grupo de rotas.

Deixo claro que este artigo tem fins educacionais e ele não deve ser a única forma de controlar o acesso à api.

---

## Apresentação do cenário

Controlar a quantidade de requisições que sua aplicação recebe é uma estratégia interessante porque evita excessos por parte das aplicações cliente.

Imaginando um cenário onde nossa aplicação tem informações atualizadas sobre emissão de documentos oficiais, ela receberá requisições de muitas aplicações ao mesmo tempo com o objetivo de atualizar seus próprios sistemas.

Este é cenário bastante comum.

Ocorre que, dado o grande volume de informações que precisam ser processadas, nossa aplicação levará um certo tempo para responder a cada requisição.

Dentre as aplicações que estão acessando nosso serviço há uma que faz requisições a cada 5 segundos.

Realizando uma conta rápida, essa aplicação sozinha fará 12 requisições por minuto, 720 por hora e 17 mil por dia.

Para evitar excessos e não prejudicar as outras aplicações vamos implementar um bloqueio para aplicações que fizerem mais de 5 requisições por minuto.

---

## Ferramentas necessárias

- [PHP 8](https://www.php.net/downloads.php) ou superior
- [Composer](https://getcomposer.org/) versão 2.0 ou superior
- Editor de textos

Existem instaladores que trazem embutidos o PHP, MySQL e Apache (servidor web), como [WAMPSERVER](https://www.wampserver.com/), [XAMPP](https://sourceforge.net/projects/xampp/) e [Laragon](https://laragon.org/). Deixo a seu critério a escolha de alguns desses pacotes, ou outro que seja familiar a você.

Para o editor de textos estou utilizando o [Visual Studio Code](https://code.visualstudio.com/download). Além de ser gratuito possibilita a instalação de diversas extensões.

---


## Projeto

O projeto a ser desenvolvido é uma aplicação Laravel conhecido framework de desenvolvimento web e está, no momento em que escrevo este artigo, na versão 9.

Acessando o terminal de sua preferência, vamos digitar o seguinte comando:

```
composer create-project --prefer-dist laravel/laravel ratelimit
```

---

O tempo de conclusão desta etapa pode variar de acordo com a velocidade de conexão à internet e a configuração do computador.

Assim que esta etapa for concluída precisamos acessar o diretório do projeto.

---

> Middleware

Dentro do diretório do projeto, vamos criar o middleware com o seguinte comando:

```
php artisan make:middleware CustomRateLimit
```
Por padrão em aplicações Laravel o middleware criado com este comando será salvo em **App\Http\Middleware**.

Utilizando o editor de textos da sua preferência vamos codificar o método `handle()` conforme a seguir:

```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

class CustomRateLimit
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
     */
    public function handle(Request $request, Closure $next)
    {
        $executed = RateLimiter::attempt(
            'education'.$request->ip(),
            $perMinute = 5,
            function() {
            }
        );

        if (!$executed) {
            return response(
                [
                    'message' => 'Too many attempts.. try again in '.RateLimiter::availableIn('education').' seconds'
                ], 429);
        }
        return $next($request);
    }
}

```

Explicando o código, criamos um limitador de requisições através do construtor `attempt` que possui 3 parâmetros obrigatórios e 1 parâmetro opcional.

O primeiro parâmetro recebe o nome do limitador. 

Este nome deve ser único e para atender esta necessidade vamos  concatenar o texto 'education' com o ip obtido através da variável $request.

OBS.: a classe **Illuminate\Http\Request** encapsula todas as informações de uma requisição http durante seu ciclo de vida.

No segundo parâmetro do construtor `attempt `informamos o limite de requisições por minuto.

O terceiro parâmetro recebe uma função de callback. A implementação desta função não é obrigatória para validação das requisições. Neste caso, informamos somente o cabeçalho do método, sem implementação.

O último parâmetro é opcional. Nele alteramos o intervalo, em segundos, para verificar as requisições de entrada. 

Por exemplo, se quisermos bloquear por 5 minutos, o valor a ser informado nesse parâmetro será 300, ou 60 segundos vezes 5 minutos.

---

> Controller

Continuando, vamos criar uma controller.

```
php artisan make:controller RateLimitController
```

Por padrão as controllers criadas com o comando acima ficam salvas em **App\Http\Controllers**.

Editando a controller vamos implementar um método para reiniciar o contador de requisições, desbloqueando o acesso à aplicação cliente e retornando uma mensagem customizada.

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

class RateLimitController extends Controller
{
    public function clearLimit(Request $request)
    {
        RateLimiter::clear('education'.$request->ip());

        return response(['message' => 'Attempts cleared for '.$request->ip()], 200);
    }
}

```

Explicando o código, fazemos uma chamada para o método `clear()` que recebe como parâmetro o nome do limitador.

Perceba que o nome é o mesmo informado na implementação do método `handle(Request $request, Closure $next)` do middleware.

---

> Rota

Agora vamos abrir o arquivo de rotas, adicionando duas novas rotas.


```php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\CustomRateLimit;
use App\Http\Controllers\RateLimitController;

Route::middleware([CustomRateLimit::class])->get('/rate-limit', function() {
    $date = new \DateTime();
    $date->setTimezone(new \DateTimeZone('-0300'));
    return $date->format('Y-m-d H:i:s');
});

Route::post('/rate-limit/clear', [RateLimitController::class, 'clearLimit']);

```

A primeira rota serve para incrementarmos o número de requisições de forma a atingir o limite que estabelecemos no middleware.

Assim que o limite for alcançado mostraremos uma mensagem customizada informando quanto tempo (em segundos) falta para que o aplicação cliente possa fazer novas requisições.

A segunda rota tem como objetivo zerar o contador através de uma chamada para o método clearLimit() da controller RateLimitController.

---


## Testando

Acessando novamente o terminal de sua preferência, na pasta do projeto, vamos iniciar o servidor na porta 8108.

Dica: dentro do VSCode, utilizando a combinação de teclas Ctrl+' um terminal será aberto na parte inferior da IDE, já na pasta do projeto.

```
php artisan serve --port=8108
```


Utilizando a extensão Thunder para VSCode, vamos fazer requisições na primeira rota até o limite de tentativas ser atingido.

E em seguida, vamos fazer uma requisição para a rota que reinicia o contador de tentativas.



{% embed https://youtu.be/YywXKg7w4ng %}

---


## Observações

Por padrão, aplicações Laravel tem o cache pré configurado para arquivo. 

Entretanto é possível mudar a configuração para uso com bancos de dados, [Memcached ](https://memcached.org/) ou [Redis](https://redis.io/), alterando a chave `'default'` da classe 
**Config\Cache.php**.

---

## Conclusão
O objetivo deste artigo foi mostrar como implementar uma forma de evitar excessos ao acessar sua aplicação, bloqueando temporariamente algumas aplicações sem parar o serviço e nem prejudicar outras aplicações.

Reforço que esta abordagem não deve ser tratada como única opção de segurança para sua aplicação

Obrigado pela leitura e até breve.

---

## Extensões utilizadas

Em desenvolvimento web, uma boa ferramenta para testar requisições api faz toda diferença.
Por isso deixo aqui o link para a extensão para VSCode [Thunder RESTClient](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client).

A extensão é gratuita e bem robusta.

---

Já para aplicações Laravel, a extensão [Laravel Extra Intellisense](https://marketplace.visualstudio.com/items?itemName=amiralizadeh9480.laravel-extra-intellisense) ajuda muito na inclusão automática de referências.

---

## Repositório público

O projeto está publicado no github [neste repositório](https://github.com/MarcioPolicarpo/laravel9-rate-limit).