PHP

Middleware per API REST in PHP: CORS, rate limiting e risposte uniformi

calendar_today personTeam EGSOFT schedule5 min di lettura
info

Disclaimer: le informazioni pubblicate in questa sezione hanno finalità di pura divulgazione tecnica. EG Software S.r.l. non si assume alcuna responsabilità per un utilizzo improprio dei contenuti, né per eventuali danni diretti o indiretti derivanti dalla loro applicazione, e non garantisce l'aggiornamento, l'accuratezza o la completezza delle informazioni riportate. Prima di utilizzare in produzione codice o procedure qui descritte, verificane sempre l'adeguatezza al proprio contesto.

Man mano che un'API REST scritta in PHP cresce, capita spesso che ogni endpoint ripeta lo stesso blocco di codice: intestazioni CORS, controllo del metodo HTTP, validazione del JSON in ingresso, formato della risposta di errore. Estrarre questa logica ripetuta in un piccolo livello middleware, richiamabile da ogni script, riduce gli errori di distrazione e rende l'API più coerente. Vediamo come costruirne uno essenziale in PHP puro, senza framework.

Header di sicurezza e CORS sotto controllo

Alcuni header HTTP andrebbero applicati a ogni risposta dell'API a prescindere dall'endpoint: X-Content-Type-Options, X-Frame-Options, una policy di Referrer-Policy prudente. Lo stesso vale per il CORS: in sviluppo si può accettare qualunque origine, ma in produzione conviene leggere l'elenco dei domini autorizzati da una variabile d'ambiente e validare l'header Origin della richiesta contro quella lista, invece di rispondere sempre con un generico asterisco.

<?php

declare(strict_types=1);

final class ApiMiddleware
{
    public static function applySecurityHeaders(): void
    {
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: DENY');
        header('Referrer-Policy: strict-origin-when-cross-origin');
    }

    public static function applyCors(): void
    {
        $originiAmmesse = explode(',', $_ENV['CORS_ALLOWED_ORIGINS'] ?? '*');
        $origine = $_SERVER['HTTP_ORIGIN'] ?? '';

        if (in_array('*', $originiAmmesse, true)) {
            header('Access-Control-Allow-Origin: *');
        } elseif (in_array($origine, $originiAmmesse, true)) {
            header("Access-Control-Allow-Origin: $origine");
        }

        header('Content-Type: application/json; charset=utf-8');
        header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
        header('Access-Control-Allow-Headers: Content-Type, Authorization');
    }

    public static function handlePreflight(): void
    {
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            self::applyCors();
            http_response_code(200);
            exit;
        }
    }

    public static function applyRateLimit(int $maxRichieste = 100, int $finestraSecondi = 3600): void
    {
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'sconosciuto';
        $file = sys_get_temp_dir() . '/ratelimit_' . md5($ip) . '.json';

        $dati = is_file($file)
            ? json_decode((string) file_get_contents($file), true)
            : ['count' => 0, 'reset' => time() + $finestraSecondi];

        if (time() > $dati['reset']) {
            $dati = ['count' => 0, 'reset' => time() + $finestraSecondi];
        }

        $dati['count']++;
        file_put_contents($file, json_encode($dati));

        if ($dati['count'] > $maxRichieste) {
            self::respondError(429, 'Troppe richieste, riprova più tardi');
        }
    }

    public static function validateJsonInput(bool $obbligatorio = false): ?array
    {
        $input = file_get_contents('php://input');

        if ($input === '' || $input === false) {
            if ($obbligatorio) {
                self::respondError(400, 'Corpo JSON obbligatorio');
            }
            return null;
        }

        $dati = json_decode($input, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            self::respondError(400, 'JSON non valido: ' . json_last_error_msg());
        }

        return $dati;
    }

    public static function respondError(int $codiceHttp, string $messaggio): never
    {
        http_response_code($codiceHttp);
        echo json_encode(['status' => 'error', 'message' => $messaggio]);
        exit;
    }

    public static function respondSuccess(string $messaggio, mixed $dati = null, int $codiceHttp = 200): void
    {
        http_response_code($codiceHttp);
        echo json_encode(['status' => 'success', 'message' => $messaggio, 'data' => $dati]);
    }
}

Limitare le richieste per IP

Un rate limiting anche semplice, basato su un contatore per indirizzo IP salvato in un file o in una cache come Redis, protegge l'API da un client che, per errore o per abuso, la interroga centinaia di volte al secondo. Non serve una soluzione sofisticata per iniziare: un contatore con finestra temporale scorrevole, salvato anche solo su file temporaneo come nell'esempio sopra, è già un primo livello di protezione utile prima di passare a soluzioni più solide in produzione.

Validare l'input e uniformare le risposte

Anche la lettura del corpo della richiesta si presta a essere centralizzata: decodificare il JSON, verificare che non ci siano errori di parsing con json_last_error() e restituire un 400 con un messaggio chiaro se il formato non è quello atteso. Allo stesso modo, avere due soli metodi, uno per le risposte di successo e uno per gli errori, che tutti gli endpoint richiamano, garantisce che il client dell'API riceva sempre lo stesso formato JSON, con gli stessi nomi di campo, qualunque cosa succeda internamente.

Usare il middleware in un endpoint

Con questi metodi statici a disposizione, un endpoint concreto si riduce a poche righe: applicare la sicurezza, gestire il preflight, validare l'input, eseguire la logica specifica e rispondere.

<?php

declare(strict_types=1);

require __DIR__ . '/ApiMiddleware.php';

ApiMiddleware::applySecurityHeaders();
ApiMiddleware::handlePreflight();
ApiMiddleware::applyCors();
ApiMiddleware::applyRateLimit();

$input = ApiMiddleware::validateJsonInput(true);

// ... logica specifica dell'endpoint ...

ApiMiddleware::respondSuccess('Operazione completata', ['id' => 42]);
Altri articoli su PHP grid_viewTutte le tematiche
Scrivici su WhatsApp