API REST con PHP 8: prácticas seguras y rendimiento práctico

php API REST con PHP 8: prácticas seguras y rendimiento práctico

API REST con PHP 8: prácticas seguras y rendimiento práctico

En este artículo verás un conjunto de patrones prácticos para construir una API REST con PHP 8: conexión segura a la base de datos con PDO, autenticación JWT, validación, manejo de errores y una aproximación ligera a limitación de peticiones. Todo orientado a código utilizable en producción.

Requisitos

  • PHP 8.0+ (recomendado 8.1/8.2)
  • ext-pdo y driver de base de datos (p. ej. pdo_mysql)
  • composer (para firebase/php-jwt)

Instalación de dependencia JWT:

composer require firebase/php-jwt

Estructura mínima

public/
  index.php    # punto de entrada
src/
  Config.php
  Database.php
  Repository/UserRepository.php
  Auth/AuthService.php
  Middleware/AuthMiddleware.php
  RateLimiter.php

1) Config y conexión PDO (segura)

declare(strict_types=1);

namespace App;

class Config {
    public static string $dsn;
    public static string $dbUser;
    public static string $dbPass;
    public static string $jwtSecret;
    public static function init(): void {
        self::$dsn = getenv('DB_DSN') ?: 'mysql:host=127.0.0.1;dbname=test;charset=utf8mb4';
        self::$dbUser = getenv('DB_USER') ?: 'root';
        self::$dbPass = getenv('DB_PASS') ?: '';
        self::$jwtSecret = getenv('JWT_SECRET') ?: 'change_me';
    }
}
declare(strict_types=1);

namespace App;

use PDO;
use PDOException;

class Database {
    private PDO $pdo;

    public function __construct() {
        try {
            $this->pdo = new PDO(
                Config::$dsn,
                Config::$dbUser,
                Config::$dbPass,
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]
            );
        } catch (PDOException $e) {
            // no detalles en producción
            http_response_code(500);
            echo json_encode(['error' => 'DB connection error']);
            exit;
        }
    }

    public function pdo(): PDO { return $this->pdo; }
}

2) Repositorio de usuarios (prepared statements)

declare(strict_types=1);

namespace App\Repository;

use App\Database;

class UserRepository {
    private \PDO $pdo;

    public function __construct(Database $db) {
        $this->pdo = $db->pdo();
    }

    public function findByEmail(string $email): ?array {
        $stmt = $this->pdo->prepare('SELECT id, email, password_hash FROM users WHERE email = ?');
        $stmt->execute([$email]);
        $user = $stmt->fetch();
        return $user ?: null;
    }

    public function createUser(string $email, string $passwordHash): int {
        $stmt = $this->pdo->prepare('INSERT INTO users (email, password_hash) VALUES (?, ?)');
        $stmt->execute([$email, $passwordHash]);
        return (int)$this->pdo->lastInsertId();
    }
}

3) Servicio de autenticación con JWT

Usa JWT con HS256 y una clave larga guardada fuera del repo (env/secret manager).

declare(strict_types=1);

namespace App\Auth;

use App\Config;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class AuthService {
    public static function generateToken(int $userId): string {
        $now = time();
        $payload = [
            'iat' => $now,
            'exp' => $now + 3600, // 1 hora
            'sub' => $userId,
        ];
        return JWT::encode($payload, Config::$jwtSecret, 'HS256');
    }

    public static function verifyToken(string $jwt): ?array {
        try {
            $decoded = (array) JWT::decode($jwt, new Key(Config::$jwtSecret, 'HS256'));
            return $decoded;
        } catch (\Throwable $e) {
            return null;
        }
    }
}

4) Middleware de autenticación (simple)

declare(strict_types=1);

namespace App\Middleware;

use App\Auth\AuthService;

class AuthMiddleware {
    public static function getUserIdFromHeader(): ?int {
        $hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
        if (!str_starts_with($hdr, 'Bearer ')) return null;
        $token = substr($hdr, 7);
        $data = AuthService::verifyToken($token);
        return $data['sub'] ?? null;
    }
}

5) Front controller: login y ruta protegida

// public/index.php
declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use App\Config;
use App\Database;
use App\Repository\UserRepository;
use App\Auth\AuthService;
use App\Middleware\AuthMiddleware;

Config::init();
$db = new Database();
$userRepo = new UserRepository($db);

header('Content-Type: application/json; charset=utf-8');

$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($method === 'POST' && $uri === '/login') {
    $body = json_decode(file_get_contents('php://input'), true);
    $email = $body['email'] ?? '';
    $password = $body['password'] ?? '';

    if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !is_string($password)) {
        http_response_code(400);
        echo json_encode(['error' => 'Invalid input']);
        exit;
    }

    $user = $userRepo->findByEmail($email);
    if (!$user || !password_verify($password, $user['password_hash'])) {
        http_response_code(401);
        echo json_encode(['error' => 'Invalid credentials']);
        exit;
    }

    $token = AuthService::generateToken((int)$user['id']);
    echo json_encode(['token' => $token]);
    exit;
}

if ($method === 'GET' && $uri === '/me') {
    $userId = AuthMiddleware::getUserIdFromHeader();
    if (!$userId) {
        http_response_code(401);
        echo json_encode(['error' => 'Unauthorized']);
        exit;
    }
    // cargar datos del usuario
    $stmt = $db->pdo()->prepare('SELECT id, email FROM users WHERE id = ?');
    $stmt->execute([$userId]);
    $user = $stmt->fetch();
    echo json_encode(['user' => $user]);
    exit;
}

http_response_code(404);
echo json_encode(['error' => 'Not found']);

6) Rate limiting (apróx. demo)

En producción usa Redis o un gateway (nginx, API gateway). Aquí un rate limiter por IP usando APCu si está disponible:

declare(strict_types=1);

namespace App;

class RateLimiter {
    private int $limit;
    private int $window;

    public function __construct(int $limit = 60, int $window = 60) {
        $this->limit = $limit;
        $this->window = $window;
    }

    public function allow(string $key): bool {
        if (function_exists('apcu_fetch')) {
            $cacheKey = 'rl:' . $key;
            $data = apcu_fetch($cacheKey);
            $now = time();
            if (!$data) {
                apcu_store($cacheKey, ['count' => 1, 'start' => $now], $this->window);
                return true;
            }
            if ($now - $data['start'] > $this->window) {
                apcu_store($cacheKey, ['count' => 1, 'start' => $now], $this->window);
                return true;
            }
            if ($data['count'] >= $this->limit) return false;
            apcu_store($cacheKey, ['count' => $data['count'] + 1, 'start' => $data['start']], $this->window - ($now - $data['start']));
            return true;
        }
        // fallback NO-SEGURO: permite siempre (multi-process no retenido)
        return true;
    }
}

7) Manejo de errores centralizado

set_exception_handler(function(\Throwable $e) {
    http_response_code(500);
    error_log($e->getMessage());
    echo json_encode(['error' => 'Internal server error']);
});

set_error_handler(function($severity, $message, $file, $line) {
    throw new ErrorException($message, 0, $severity, $file, $line);
});

Buenas prácticas rápidas

  • Guarda secretos fuera del repo (env vars, vaults). No uses JWT long-lived sin refresh tokens.
  • Usa password_hash() y password_verify(); configura cost si hace falta.
  • Valida y sanitiza toda entrada; usa prepared statements siempre.
  • Usa TLS para todo el tráfico; marca cookies secure y SameSite si usas cookies.
  • Registra fallos de autenticación con límites para detectar ataques de fuerza bruta.

Siguiente paso práctico: implementa refresh tokens almacenados en base de datos (rotación y revocación), añade soporte CORS restringido y mueve el rate limiter a Redis o al nivel del API gateway.

Advertencia de seguridad: no confíes en implementaciones en memoria para rate limiting en entornos multi-process o distribuidos; tampoco expongas la clave JWT en repositorios ni uses algoritmos sin verificar el campo alg del token. Considera rotación y almacenamiento seguro de claves.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión