API rate limiting biztonságos webes szolgáltatás kialakítása.

Rate Limiting: Amikor az API-dnak szüksége van egy kis „lassító sávra”

Szóval épp egy publikus webes API szolgáltatáson dolgozol, ami egyre népszerűbb lesz. Az első öt percben minden csodálatosan működik, majd hirtelen jön a telefonhívás: „Az API lelassult, valaki percenként 10 ezer kérést küld!” Ismerős? Pontosan erre a problémára találták ki a rate limitinget, ami lényegében egy intelligens forgalomszabályozó a backend kapujában.

Miért nem csak egy sima „if” elágazás?

Rate limiting első hallásra egyszerűnek tűnik: „Csak számold a kéréseket, és ha túl sok, küldj egy hibát!” A valóságban azonban egy kritikus biztonsági és stabilitási mechanizmus. Nem csak arról van szó, hogy megvédjük a szerverünket a túlterheléstől (ami önmagában is elég fontos), hanem:

– Megakadályozzuk, hogy egyetlen felhasználó (akár véletlen, akár rosszindulatú) ellepje a sávszélességed – Garantáljuk az igazságos erőforrás-elosztást minden felhasználó között – Védjük a mögöttes adatbázisunkat és szolgáltatásainkat a DDoS-szerű támadásoktól – Akár különböző csomagokat (ingyenes, prémium) is tudjunk kezelni ennek segítségével

Egy alapvető PHP backend implementáció

A PHP világában a rate limiting implementációja gyakran a munkamenet-kezelésre vagy gyorsítótárra épül. Íme egy egyszerű, de hatékony megközelítés token bucket algoritmussal:

class RateLimiter {
    private $cache;
    private $limit;
    private $window;
    
    public function __construct($limit = 100, $window = 3600) {
        // Használhatunk Redis-t, Memcached-et vagy akár fájl alapú cache-t
        $this->cache = new Redis();
        $this->cache->connect('127.0.0.1', 6379);
        $this->limit = $limit;
        $this->window = $window; // másodpercben
    }
    
    public function check($apiKey) {
        $key = "rate_limit:$apiKey";
        $current = $this->cache->get($key);
        
        if ($current === false) {
            // Első kérés ebben az ablakban
            $this->cache->setex($key, $this->window, 1);
            return true;
        }
        
        if ($current >= $this->limit) {
            // Limitet túllépték
            return false;
        }
        
        // Növeljük a számlálót
        $this->cache->incr($key);
        return true;
    }
    
    public function getRemaining($apiKey) {
        $key = "rate_limit:$apiKey";
        $current = $this->cache->get($key) ?: 0;
        return max(0, $this->limit - $current);
    }
}

// Használat:
$limiter = new RateLimiter(100, 3600); // 100 kérés/óra
$apiKey = $_SERVER['HTTP_API_KEY'] ?? '';

if (!$limiter->check($apiKey)) {
    http_response_code(429); // Too Many Requests
    echo json_encode(['error' => 'Rate limit exceeded. Try again later.']);
    exit;
}

// API logika folytatódik...

Frontend: Hogyan kommunikáljunk a felhasználóval?

Amikor a backend 429-es hibát küld, a frontendnek intelligensen kell kezelnie ezt. Itt egy jQuery alapú megoldás, ami Bootstrap komponensekkel illusztrálja a státuszt:

$(document).ready(function() {
    let rateLimitRemaining = 100;
    let rateLimitReset = null;
    
    function makeApiCall() {
        $.ajax({
            url: '/api/endpoint',
            method: 'GET',
            headers: {
                'API-Key': 'user_api_key_here'
            },
            success: function(data, textStatus, xhr) {
                // Frissítjük a rate limit infókat a header-ekből
                rateLimitRemaining = xhr.getResponseHeader('X-RateLimit-Remaining') || rateLimitRemaining;
                rateLimitReset = xhr.getResponseHeader('X-RateLimit-Reset');
                
                updateRateLimitDisplay();
            },
            error: function(xhr) {
                if (xhr.status === 429) {
                    // Rate limit túllépve
                    const resetTime = xhr.getResponseHeader('X-RateLimit-Reset');
                    showRateLimitAlert(resetTime);
                    
                    // Automatikus újrapróbálkozás a reset idő után
                    const waitTime = resetTime ? 
                        (new Date(resetTime * 1000) - new Date()) : 
                        60000;
                    
                    setTimeout(makeApiCall, waitTime);
                }
            }
        });
    }
    
    function updateRateLimitDisplay() {
        const $indicator = $('#rateLimitIndicator');
        const percentage = (rateLimitRemaining / 100) * 100;
        
        // Bootstrap progress bar frissítése
        $indicator.css('width', percentage + '%');
        $indicator.text(rateLimitRemaining + ' kérés maradt');
        
        // Szín változtatás a kihasználtság alapján
        $indicator.removeClass('bg-success bg-warning bg-danger');
        if (percentage > 50) {
            $indicator.addClass('bg-success');
        } else if (percentage > 20) {
            $indicator.addClass('bg-warning');
        } else {
            $indicator.addClass('bg-danger');
        }
    }
    
    function showRateLimitAlert(resetTime) {
        // Bootstrap alert komponens
        const $alert = $(`
            <div class="alert alert-warning alert-dismissible fade show" role="alert">
                Túl sok kérést küldött! Várjon amíg újra használhatja az API-t.
                ${resetTime ? `Újrapróbálás: ${new Date(resetTime * 1000).toLocaleTimeString()}` : ''}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        `);
        
        $('#alertsContainer').append($alert);
        
        // Alert automatikus eltüntetése
        setTimeout(() => $alert.alert('close'), 10000);
    }
    
    // Kezdeti állapot
    updateRateLimitDisplay();
});

És egy kis SCSS a progress bar testreszabásához:

#rateLimitIndicator {
    transition: width 0.3s ease, background-color 0.3s ease;
    font-size: 0.8rem;
    font-weight: bold;
    
    &.bg-success {
        box-shadow: 0 0 5px rgba($success, 0.5);
    }
    
    &.bg-warning {
        box-shadow: 0 0 5px rgba($warning, 0.5);
    }
    
    &.bg-danger {
        animation: pulse 1s infinite;
    }
}

@keyframes pulse {
    0% { opacity: 1; }
    50% { opacity: 0.7; }
    100% { opacity: 1; }
}

Gyakori buktatók, amiket jobb előre tudni

1. Túl egyszerű számláló: Ha csak egy sima számlálót növelgetsz, akkor egy felhasználó az ablak elején elküldheti az összes kérést, majd várhat a következő ablakra. A sliding window vagy token bucket algoritmusok sokkal igazságosabbak.

2. Felejtetted el a reset headereket: A felhasználónak tudnia kell, mikor próbálkozzon újra. Az X-RateLimit-Reset header (UNIX timestamp formátumban) elengedhetetlen.

3. Minden endpointra ugyanaz a limit: Egy GET /users endpointnak és egy POST /transactions endpointnak lehet, hogy más a terhelése. Implementálj eltérő limiteket különböző útvonalakra.

4. LocalStorage/SessionStorage használata backend számláláshoz: Ez egy gyakori hibalehetőség. Ne feledd: a frontend kódot a felhasználó manipulálhatja. Minden számlálást a backendben kell végezni.

5. IP cím alapú limit túlzott használata: A NAT vagy proxy mögül több felhasználó is ugyanazzal az IP címmel érkezhet. Inkább API kulcsok alapján limitálj, ha lehetséges.

Összegzés

A rate limiting nem egy „nice-to-have” feature, hanem egy alapvető biztonsági és stabilitási követelmény minden publikus API szolgáltatásnál. Minél korábban implementálod a fejlesztési folyamatban, annál kevesebb fejfájást okoz később.

A kulcs a mértékletesség: a limitnek elég lazának kell lennie, hogy a normál felhasználók ne érezzék, de elég szigorúnak, hogy megvédjen a visszaélésektől. És ne feledd: mindig kommunikáld egyértelműen a felhasználóval, mi történik, amikor eléri a limitet – semmi sem frusztrálóbb, mint egy titokzatos hiba, ami „csak néha” jön elő.

A végére egy bölcsesség: a legjobb rate limiting az, amiről a felhasználóid nem is tudnak – amíg nem próbálnák meg túlterhelni a rendszeredet. Akkor viszont nagyon is érezniük kell a hatását.