Capa de post contendo um celular sob um livro que está sob um notebook e uma corrente envolvendo (prendendo) todos os itens

Rate Limit com Laravel


laravel-8 novidades guias

Em algum momento nos deparamos com a necessidade de limitar a quantidade de requisições em um determinado ponto do sistema, e agora? Conheça o Rate Limit, o limitador de requisições do Laravel!

O que é Rate Limit

Este recurso existe no laravel desde a versão 5.2 e teve agora na versão 8 um incremento bastante interessante.

Para quem não sabe como funciona um limitador de requisições: basicamente o que ele faz é determinar quantas requisições um usuário pode realizar em um período determinado de tempo, por exemplo, um usuário não pode executar mais do que 10 requisições no período de um minuto, quando a quantia de requisições neste período for excedida o usuário irá receber algum tipo de bloqueio e não poderá mais realizar requisições durante algum tempo e é isso que é o limitador do Laravel faz para a gente.

Rate Limit no Laravel

O recurso de rate limit geralmente é implementado em APIs, no Laravel a implementação deste conceito foi através de um middleware e por isso também pode ser utilizado em rotas web ou em qualquer grupo de rotas. Existe também um limitador relacionado aos Jobs mas aqui vamos focar em rotas.

No Laravel o limite de requisições (rate limit) pode ser controlado através de um middleware chamado Throttle que já vem por padrão com o framework. Para a gente conseguir utilizar o middleware basta fazer a atribuição nas rotas ou no grupo de rotas, nessa atribuição podemos dizer qual que é a quantia de requisições que o usuário vai poder utilizar e qual é o período de tempo, vamos utilizar como parâmetro por exemplo 10 requisições a cada 60 segundos.

O que o middleware faz é basicamente adicionar um header na response indicando quantas requisições o usuário já fez, qual é o limite e quantas ele ainda pode fazer, a partir deste header o Laravel consegue determinar se o limite foi excedido ou não.

Uma vez que o limite foi excedido o middleware se encarrega de adicionar uma nova resposta automaticamente ao retorno da requisição: é retornado o código 429 que representa que o servidor recebeu muitas requisições (too many requests).

Utilizando o Throttle middleware

O ThrottleMiddleware pode ser atribuído tanto a rotas individuais ou grupos, isso significa que, caso seja necessário, podemos atribuir o middleware diretamente no grupo api e assim todas as rotas de API passarão pelas regras de quantidades de requisições.

Rate Limit no Laravel 7.x ou anterior

Até a versão 7.x a forma como podíamos utilizar o rate limit era basicamente passando os parâmetros em forma de string na própria definição de middlewares da rota ou grupo de rotas, basta utilizar o nome do middleware que neste caso é o throttle. O primeiro parâmetro é referente a quantia de requisições e o segundo parâmetro referente a medida de tempo, veja como fica a utilização em uma rota de API onde o limite são 10 requisições por minuto:

// arquivo routes/api.php

use Illuminate\Support\Facades\Route;

Route::middleware('api', 'throttle:10,1')
    ->get('/',
        fn() => response()
            ->json([
                'message' => 'Hello Middleware'
            ])
        );

Aqui foi definido que na rota / da nossa API um usuário poderá realizar no máximo 10 requisições no período de 1 minuto. Quando este limite for excedido o usuário receberá uma resposta de status code 429:

Print com os dizeres '429 | TOO MANY REQUESTS' indicando o status code 429

Também podemos observar os headers da resposta e encontraremos os limites, quantas requisições faltam para atingir o limite, quantos segundos faltam para o limite resetar e um timestamp de quando o limite irá resetar:

Server: nginx/1.14.0 (Ubuntu)
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
x-ratelimit-limit: 10
x-ratelimit-remaining: 0
retry-after: 52
x-ratelimit-reset: 1608817145
Cache-Control: no-cache, private
date: Thu, 24 Dec 2020 13:38:14 GMT

Entendendo os headers

x-ratelimit-limit: 10 indica que o limite para aquela rota é de 10 requisições

x-ratelimit-remaining: 0 indica quantas requisições no período ainda podem ser executadas

retry-after: 52 indica quantos segundos faltam para que o limite seja resetado

x-ratelimit-reset: 1608817145 mostra o timestamp de quando o limite será resetado

Estes headers estarão presentes em todas as requisições que excederem o limite definido. Requisições que ainda não excederam o limite recebem apenas os headers x-ratelimit-limit e x-ratelimit-remaining.

Este comportamento não foi alterado na nova versão do Laravel então nos demais exemplos estes dados serão omitidos.

Utilizando o RateLimiter

Nas versões anteriores a versão 8 do Laravel, o middleware throttle podia ser usado de forma simples conforme vimos a pouco. Este método ainda funciona e não há problema algum utilizá-lo, inclusive não há nenhum aviso de deprecated por parte do Laravel ou coisa do gênero.

Na versão 8 foi introduzida a facade RateLimiter e com ela podemos definir limites nomeados e com uma interface fluida também podemos criar configurações mais elaboradas. Para sua utilização ao invés de passar os limites como parâmetro para o ThrottleMiddleware, podemos passar o nome do RateLimiter.

Um RateLimiter geralmente é configurado no método configureRateLimiting na classe RouteServiceProvider.

protected function configureRateLimiting()
{
    RateLimiter::for('api', fn(Request $request) => Limit::perMinute(10));
}

Neste exemplo está sendo definido que o limite das rotas de API é de 10 requisições por minuto, ou seja, todas as rotas do sistema estão com a proteção de limite de requisições e caso o usuário exceda o limite receberá a resposta de muitas requisições (status code 429).

Também é possível customizar a resposta quando um limite é excedido, para isso basta passarmos um callback para o método response() da classe Limit:

RateLimiter::for('api', fn(Request $request) =>
            Limit::perMinute(2)
                ->response(fn() => response('Voce excedeu a cota de requisicoes', 429)));

Neste momento, ao invés de exibir a tela padrão de respostas 429 que o Laravel nos provê, estamos mudando para uma string indicando que o usuário excedeu suas cotas de requisições.

Como no callback foi utilizado a função response() do Laravel, temos todo o acesso que teríamos em controllers e podemos retornar uma resposta como já estamos acostumados. Vamos retornar um json ao invés de uma string:

RateLimiter::for('api', fn(Request $request) =>
            Limit::perMinute(2)
                ->response(fn() => response()
                        ->json([ 'message' => 'Voce excedeu a cota de requisicoes'], 429)
                    )
        );

Controles de limite por tempo

Uma vantagem desta nova abordagem é um maior controle nos limites de tempo, nas versões anteriores o limite de requisições era controlado por minuto mas com o RateLimiter agora temos opções de limites por minuto, por hora, por dia e ilimitado, além disso podemos combinar os limites da forma que melhor atender as regras de negócio utilizando múltiplos limitadores, para isso basta retornar um array no callback da facade RateLimiter, a ordem de precedência é a ordem em que os limitadores estão posicionados no array:

RateLimiter::for('api', fn(Request $request) => [
                Limit::perMinute(10),
                Limit::perHour(100)
            ]
        );

No exemplo acima o framework irá sempre verificar primeiro se o limite por minuto já foi excedido e em caso contrário verificará se o limite por hora foi excedido. Também é possível customizar as mensagens de cada limite mesmo utilizando um array:

RateLimiter::for('api', fn(Request $request) => [
                Limit::perMinute(10)->response(fn() => response('Limite por minuto excedido', 429)),
                Limit::perHour(100)->response(fn() => response('Limite por hora excedido', 429))
            ]
        );

Limitação por segmento

Em alguns casos de uso pode ser necessário limitar a quantidade de requisições por segmento, por usuário logado ou convidado ou até mesmo de acordo com planos de utilização do seu sistema. O Laravel já suporta este tipo de segmentação e como no callback temos acesso a Request podemos pegar o usuário logado ou convidado, podemos utilizar o ip da requisição etc.

RateLimiter::for('api', fn (Request $request) =>
            $request->user()?->vipCustomer()
                        ? Limit::none()
                        : Limit::perMinute(10)->by($request->ip())
        );

No exemplo acima estamos definindo que caso existe um usuário logado e ele seja vip não haverá limite de requisições, caso contrário haverá um limite de 10 requisições por minuto. Assim como nos exemplos anteriores é possível adicionar uma mensagem customizada na resposta da requisição:

RateLimiter::for('api', fn (Request $request) =>
            $request->user()?->vipCustomer()
                        ? Limit::none()
                        : Limit::perMinute(2)
                            ->by($request->ip())
                            ->response(fn() => response()
                                ->json([
                                    'message' => 'Limite de requisicoes excedido, para ter acesso ilimitado contrate o plano vip'
                                ], 429)
                            )
        );

Middlewares customizados e Rate Limit

Até aqui vimos apenas como adicionar o limitador de requisições nas rotas que utilizam os middlewares padrão do Laravel mas também é possível adicionar limites a middlewares customizados de forma bastante simples.

Criando um middleware

Primeiramente vamos criar um middleware customizado, podemos criar “na mão” ou executar o comando artisan mas como bons artesãos vamos utilizar o artisan para criar um middleware de logs:

php artisan make:middleware LogThrottleMiddleware

Um middleware chamado LogThrottleMiddleware foi criado na pasta app/Http/Middleware e vamos adicionar um log do tipo debug ao método handle() do middleware:

public function handle(Request $request, Closure $next)
{
    Log::debug('Um log simples');

    return $next($request);
}

O middleware está apenas escrevendo uma mensagem simples nos logs do Laravel que por padrão residem na pasta storage/logs/. Agora vamos adicionar um limitador de requisições para o novo middleware criado:

protected function configureRateLimiting()
{
    RateLimiter::for('log-throttle', fn() => Limit::perMinute(10));
}

Agora você pode estar se perguntando de onde saiu a string log-throttle já que no middleware não foi definido nada do gênero, certo? Pois é! No começo do post falamos que ao invés de utilizar o middleware throttle passando por string os limites, podemos passar limites nomeados, lembra? Para “nomear” um limitador precisamos criar um novo registro nas configurações em app/Http/Kernel.php na seção de grupo de middlewares:

// app/Http/Kernel.php

protected $middlewareGroups = [
    ...
    'api' => [
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
    'log-throttle' => [
        'throttle:log-throttle',
        LogThrottleMiddleware::class
    ]
];

No bloco acima definimos um middleware de grupo chamado log-throttle e ao utilizá-lo em rotas ou grupo de rotas toda requisição passará pelo limitador (rate limit) chamado log-throttle e também pelo middleware LogThrottleMiddleware. Anteriormente definimos que o log-throttle teria um limite de 10 requisições por minuto e que o middleware iria criar um log bem simples no diretório padrão. Agora a única coisa que falta é adicionar o grupo a uma rota ou grupo de rotas:

Adicionando um grupo customizado às rotas

Route::get('/user-ip', fn(Request $request) => response()
    ->json([
            'user' => 'Jose',
            'ip' => $request->ip(),
        ]))
    ->middleware('log-throttle');

Um log bem simples foi escrito em storage/logs/laravel.log:

[2020-12-26 16:38:24] local.DEBUG: Um log simples.

E o json da resposta também retornou corretamente o que estava no callback da rota:

{
  "user": "Jose",
  "ip": "172.27.0.1"
}

E caso o limite de requisições seja excedido também recebemos a mensagem de muitas requisições:

Print com os dizeres '429 | TOO MANY REQUESTS' indicando o status code 429

Massa, né? O mesmo funciona para grupo de rotas, basta passar o middleware que criamos para um grupo de rotas:

Route::middleware(['log-throttle'])
    ->group(function () {
        Route::get('/', fn() => response()->json(['message' => 'Hello group']));
        Route::get('/ip', fn(Request $request) => response()->json(['message' => "Seu Ip: {$request->ip()}"]));
    });

Atribuindo limitadores sem um middleware

Nem sempre será necessária a utilização de um middleware em conjunto com o limitador como no exemplo anterior. Pode ser conveniente utilizar apenas o nome do limitador em alguma rota, certo? No Laravel também é possível se beneficiar deste comportamento, vamos criar um controle de requisições para uma rota de uploads sem a utilização de um middleware para exemplificar. Começando pelo limitador:

protected function configureRateLimiting()
{
    RateLimiter::for('uploads', fn() => Limit::perMinute(2));
}

Agora vamos “registrar” o limitador de nome uploads. Diferente da criação de um grupo para atrelar o throttle a um middleware desta vez a única ação necessária é passar o parâmetro do nome do limitador na utilização do middleware throttle:

Route::get('/uploads', [UploadController::class, 'store'])
    ->middleware('throttle:uploads');

Desta forma o próprio framework se encarregará de encontrar um limitador com o nome passado pelo parâmetro e já está tudo pronto!

Conclusão

O limitador de requisições (rate limiter) pode ser uma mão na roda na construção de aplicações com este tipo de regra. Pode ajudar tanto a “proteger” rotas muito acessadas ou mais sensíveis quanto ajudar na criação de planos de venda que limitam a quantidade de requisições que um cliente pode ou não realizar.

Vale lembrar que apesar de o limitador impedir que um usuário acesse o conteúdo da rota desejada caso o limite seja excedido, ele não serve como uma proteção para ataques como DDoS, o limitador apenas impede que o usuário acesse o conteúdo de uma rota mas não impede que muitas requisições sejam feitas ao servidor. Caso sua necessidade esteja relacionada a ataques DDoS ou similares aconselho procurar outras técnicas/ferramentas.

-- Up the Laravel's \o/

← Post Anterior
Próximo Post →