Cómo usar Rate Limiters con rutas en Laravel

Laravel

En muchas ocasiones podría interesarte limitar el acceso a ciertas rutas de Laravel, ya sea para proteger tu aplicación o por cualquier otro motivo. Por ejemplo, podrías querer limitar acceso a ciertos usuarios o IPs a cierto número de peticiones por minuto o por hora. De este modo podrías evitar que algún usuario malintencionado sature tu servidor.

Este problema es algo que me ha sucedido en DuracionDe. Se trata de una aplicación en la que cualquier usuario anónimo puede colaborar enviando datos de sus gameplays. El caso es que alguien estaba enviando datos falsos mediante un bot. Esto es algo que había cubierto usando cierto algoritmo, de modo que si los datos se desviaban de la media demasiado, los excluía. Sin embargo no había contado con que ciertos usuarios podría querer saturar esta aplicación. Para solucionar el problema, usé un Rate Limiter.

En este tutorial aprenderás primero a crear Rate Limiters y seguidamente a aplicarlos a tus rutas.

Cómo crear un Rate Limiter

Podrás crear un Rate Limiter en el método boot de algún Service Provider de Laravel. Lo recomendable es crearlos en la clase App\Providers\RouteServiceProvider. En el siguiente ejemplo vamos a crear un Rate Limiter al que daremos el nombre de almacenar. Lo que hará este Rate Limiter será limitar el acceso a las rutas asociadas a 100 veces por minuto:

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
// ...
public function boot()
{
  // ...
  RateLimiter::for('almacenar', function (Request $request) {
    return Limit::perMinute(100);
  });
}

En este ejemplo le hemos dado el nombre de almacenar al Rate Limiter, pero en realidad podríamos haber usado cualquier otro nombre. Por ejemplo , podríamos haberle llamado archivos o descargas, ya que de hecho no es un mal método para limitar el número de descargas que un usuario puede iniciar.

Puedes restringir el acceso de forma que solamente sea posible acceder a las rutas asociadas cierto número de veces por IP. En este ejemplo restringimos el acceso a 10 veces por IP cada minuto:

// ...
RateLimiter::for('almacenar', function (Request $request) {
  return Limit::perMinute(10)->by($request->ip());
});
/ ...

También puedes restringir el acceso por usuario. En este ejemplo restringimos el acceso a 10 veces por usuario y minuto:

// ...
RateLimiter::for('almacenar', function (Request $request) {
  return Limit::perMinute(10)->by($request->user()->id);
});
/ ...

También puedes aplicar diferentes límites en función de si se trata de un usuario registrado o no:

// ...
RateLimiter::for('almacenar', function (Request $request) {
  if($request->user()) {
    return Limit::perMinute(20)->by($request->user()->id);
  } else {
    return Limit::perMinute(10)->by($request->ip());
  }
});
/ ...

Cómo agrupar varios Rate Limiters

Puedes agrupar varios rate limiters en uno solo, de forma que entren en juego diferentes limitadores. Para ello basta con devolver un array de Limiters. En este ejemplo establecemos un límite global de 200 peticiones por minuto, además de otro límite de 20 peticiones por IP para usuarios anónimos:

// ...
RateLimiter::for('almacenar', function (Request $request) {
  return [
    Limit::perMinute(200);
    Limit::perMinute(20)->by($request->ip());
  ];
});
/ ...

Cómo aplicar un Rate Limiter a una ruta

Para aplicar un Rate Limiter a una ruta tendrás que usar el middleware throttle, que se usa para proteger las rutas de excesivas peticiones HTTP. No solamente podrás usar este middleware con las rutas web, sino también con las rutas API de tu proyecto.

En este ejemplo aplicamos Rate Limiter almacenar que hemos creado antes a la ruta /cosas usando el middleware throttle:

Route::middleware(['throttle:almacenar'])
  ->post('/cosas', [CosasController::class, 'store']);

Tal y como puedes ver, la sintaxis usada es muy sencilla; basta con usar usar la sentencia throttle seguida de dos puntos : y el nombre del Rate Limiter que deseas aplicar, que en nuestro ejemplo es almacenar.

En caso de superar los límites aplicados a las rutas, el usuario verá un error 429 con el texto «429 | TOO MANY REQUETS«. Tal y como veremos más adelante podrás personalizar este mensaje.

Cómo aplicar un Rate Limiter a un grupo de rutas

También puedes aplicar el Rate Limiter  a un grupo de rutas. En el siguiente ejemplo aplicamos el Rate Limiter almacenar a un grupo de rutas:

Route::middleware(['throttle:almacenar'])->group(function () {
  Route::post('/cosas', [CosasController::class, 'store']);
  Route::post('/descargas', [DescargasController::class, 'descargar'])
});

Modifica el sistema de caché de los Rate Limiters

Cuando se usa un Rate Limiter, es necesario almacenar los contadores de acceso en alguna parte. Laravel usa por defecto el sistema de caché que se usa por defecto en la aplicación que por defecto es un archivo del sistema, ya que se usa el método FILE.

Para modificar el método de almacenamiento debes editar el valor de la opción CACHE_DRIVER en el archivo .env de tu proyecto. Si por ejemplo quieres usar redis, deberás asignarle dicho valor:

CACHE_DRIVER=redis

Sin embargo, esto modificará el sistema de caché de toda la aplicación, algo que en muchos casos es deseable pero quizás en el tuyo no.

Si usas Redis como sistema de caché, has de saber que Laravel integra un sistema de caché específico optimizado para Redis que permite gestionar los limitadores de una forma más eficiente. Si accedes al archivo App\Http\Kernel, podrás comprobar que el middleware throttle está mapeado con la clase Illuminate\Routing\Middleware\ThrottleRequests. Para usar usar la clase específica de Redis debes mapear el middleware throttle con la siguiente clase:

// ...
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
// ...

Personaliza el mensaje de error

Puedes personalizar el mensaje de error que devuelve cuando se supera el límite de peticiones establecido. Para ello, debes modificar el Rate Limiter de este modo:

// ...
RateLimiter::for('almacenar', function (Request $request) {
  return Limit::perMinute(10)->response(function () {
    return response('Tu respuesta aquí...', 429);
  });
});
//...

Tal y como ves, puedes escoger tanto el mensaje de error como el código de la respuesta.

Y con esto ya hemos terminado. Esto ha sido todo amigos 👋

Si te ha gustado este tutorial, sígueme en 𝕏.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”