miércoles, 25 de junio de 2025

PHP: Implementando un Sistema de Rate Limiting Adaptativo

PHP: Implementando un Sistema de Rate Limiting Adaptativo

El rate limiting es una técnica esencial para proteger aplicaciones web contra abusos, como ataques de fuerza bruta o DoS (Denial of Service). Un enfoque común es limitar el número de solicitudes que un usuario puede realizar en un período de tiempo determinado. Este artículo explora una implementación de rate limiting adaptativo en PHP, que ajusta dinámicamente los límites basándose en el comportamiento del usuario y la carga del servidor.

La idea principal es comenzar con un límite permisivo y reducirlo si un usuario excede un umbral de error o si la carga del servidor es alta. Usaremos Redis para almacenar el historial de solicitudes y un algoritmo básico para ajustar los límites.


<?php

use Predis\Client;

class AdaptiveRateLimiter {

    private $redis;
    private $prefix = 'rate_limit:';
    private $defaultLimit = 100; // Límite inicial por minuto
    private $errorThreshold = 5; // Umbral de errores antes de reducir el límite
    private $reductionFactor = 0.5; // Factor de reducción del límite
    private $ttl = 60; // Tiempo de vida en segundos (1 minuto)

    public function __construct(Client $redis) {
        $this->redis = $redis;
    }

    public function isAllowed(string $userId): bool {
        $key = $this->prefix . $userId;
        $limit = $this->getLimit($userId);

        $count = $this->redis->incr($key);
        if ($count === 1) {
            $this->redis->expire($key, $this->ttl);
        }

        if ($count > $limit) {
            return false; // Límite excedido
        }

        return true; // Permitido
    }

    public function recordError(string $userId): void {
        $errorKey = $this->prefix . 'error:' . $userId;
        $errorCount = $this->redis->incr($errorKey);

        if ($errorCount === 1) {
            $this->redis->expire($errorKey, $this->ttl);
        }

        if ($errorCount > $this->errorThreshold) {
            $this->reduceLimit($userId);
            $this->redis->del($errorKey); // Resetear contador de errores
        }
    }

    private function getLimit(string $userId): int {
        $limitKey = $this->prefix . 'limit:' . $userId;
        $limit = $this->redis->get($limitKey);

        if ($limit === null) {
            return $this->defaultLimit;
        }

        return (int) $limit;
    }

    private function reduceLimit(string $userId): void {
        $currentLimit = $this->getLimit($userId);
        $newLimit = (int) ($currentLimit * $this->reductionFactor);

        if ($newLimit < 1) {
            $newLimit = 1; // Asegurar un límite mínimo
        }

        $limitKey = $this->prefix . 'limit:' . $userId;
        $this->redis->set($limitKey, $newLimit, 'EX', $this->ttl * 2); // Extender TTL del límite
    }
}

// Ejemplo de uso:
// $redis = new Predis\Client();
// $rateLimiter = new AdaptiveRateLimiter($redis);

// if ($rateLimiter->isAllowed('user123')) {
//     // Procesar la solicitud
//     echo "Solicitud permitida.\n";
// } else {
//     // Rechazar la solicitud
//     echo "Límite de solicitudes excedido.\n";
// }

// // Si ocurre un error (ej: autenticación fallida)
// $rateLimiter->recordError('user123');

    

El código anterior define una clase AdaptiveRateLimiter que utiliza Redis para el almacenamiento. La función isAllowed verifica si un usuario ha excedido su límite de solicitudes. La función recordError incrementa un contador de errores y, si se supera el umbral, reduce el límite del usuario utilizando la función reduceLimit. La función getLimit recupera el límite actual para el usuario, o establece un límite por defecto si aún no existe.


<?php
// Ejemplo de Integración en un Middleware (ej: Slim Framework)
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

$app->add(function (Request $request, Response $response, callable $next) use ($redis) {
    $rateLimiter = new AdaptiveRateLimiter($redis);
    $userId = $request->getHeaderLine('X-User-ID'); // Obtener el ID del usuario del header

    if (empty($userId)) {
        return $response->withStatus(400)->withJson(['error' => 'Missing User ID']);
    }

    if (!$rateLimiter->isAllowed($userId)) {
        return $response->withStatus(429)->withJson(['error' => 'Too Many Requests']);
    }

    $response = $next($request, $response);
    return $response;
});
    

Este enfoque adaptativo ofrece una mayor flexibilidad que el rate limiting estático. Permite responder dinámicamente a cambios en el comportamiento del usuario y en la carga del sistema, mejorando la disponibilidad y la seguridad de la aplicación.

No hay comentarios:

Publicar un comentario