Админка
please wait

O rate limiting protege a sua API contra abusos, assegura uma alocação justa de recursos e mantém a disponibilidade do serviço. Quer esteja a prevenir ataques DDoS ou a gerir quotas de API, um rate limiting adequado é essencial para APIs em produção. Este guia aborda a implementação de rate limiting eficaz na perspetiva de um programador sénior.

Porque o Rate Limiting é Importante

O rate limiting protege contra:

  1. Ataques DDoS: Evitar a sobrecarga do serviço
  2. Força bruta: Bloquear tentativas de credential stuffing
  3. Abuso de recursos: Utilização justa entre clientes
  4. Controlo de custos: Limitar operações dispendiosas
  5. Monetização da API: Aplicar limites do plano

Estratégias de Rate Limiting

Janela Fixa

Contar pedidos em períodos de tempo fixos (por exemplo, 100 pedidos por minuto).

Prós: Simples de implementar Contras: Picos nos limites da janela

// Node.js com Redis
const Redis = require('ioredis');
const redis = new Redis();
async function fixedWindowLimit(userId, limit, windowSeconds) {
const key = `ratelimit:${userId}:${Math.floor(Date.now() / 1000 / windowSeconds)}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSeconds);
}
return {
allowed: current <= limit,
remaining: Math.max(0, limit - current),
resetAt: Math.ceil(Date.now() / 1000 / windowSeconds) * windowSeconds
};
}

Registo de Janela Deslizante

Acompanhar os timestamps de cada pedido e contar dentro de uma janela deslizante.

Prós: Rate limiting mais suave Contras: Elevado consumo de memória com tráfego intenso

async function slidingWindowLog(userId, limit, windowSeconds) {
const key = `ratelimit:${userId}`;
const now = Date.now();
const windowStart = now - (windowSeconds * 1000);
// Remover entradas antigas e adicionar a nova
const multi = redis.multi();
multi.zremrangebyscore(key, 0, windowStart);
multi.zadd(key, now, `${now}-${Math.random()}`);
multi.zcard(key);
multi.expire(key, windowSeconds);
const results = await multi.exec();
const count = results[2][1];
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
count
};
}

Token Bucket

Tokens adicionados a uma taxa fixa; os pedidos consomem tokens.

Prós: Permite picos controlados Contras: Implementação mais complexa

async function tokenBucket(userId, bucketSize, refillRate, refillInterval) {
const key = `tokenbucket:${userId}`;
const now = Date.now();
// Obter ou inicializar o bucket
let bucket = await redis.hgetall(key);
if (!bucket.tokens) {
bucket = { tokens: bucketSize, lastRefill: now };
}
// Calcular os tokens a adicionar
const timePassed = now - parseInt(bucket.lastRefill);
const intervalsPasssed = Math.floor(timePassed / refillInterval);
const tokensToAdd = intervalsPasssed * refillRate;
const newTokens = Math.min(bucketSize, parseInt(bucket.tokens) + tokensToAdd);
// Tentar consumir um token
if (newTokens >= 1) {
await redis.hset(key, {
tokens: newTokens - 1,
lastRefill: bucket.lastRefill + (intervalsPasssed * refillInterval)
});
await redis.expire(key, 3600);
return { allowed: true, remaining: newTokens - 1 };
}
return { allowed: false, remaining: 0 };
}

Middleware Express.js

Implementação Básica

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redis = new Redis();
// Rate limiter básico
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 100, // 100 pedidos por minuto
standardHeaders: true, // Devolver informação de rate limit nos cabeçalhos
legacyHeaders: false,
message: {
error: 'Too many requests',
retryAfter: 60
}
});
app.use('/api', limiter);

Com Redis para Sistemas Distribuídos

const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: 'rl:'
}),
keyGenerator: (req) => {
// Usar o ID do utilizador se estiver autenticado; caso contrário, o IP
return req.user?.id || req.ip;
},
skip: (req) => {
// Ignorar o rate limiting em determinadas condições
return req.user?.role === 'admin';
},
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
});
}
});

Vários Rate Limiters

// Limiter restrito para endpoints de autenticação
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 5, // 5 tentativas por 15 minutos
message: { error: 'Too many login attempts' }
});
// Limiter padrão da API
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100
});
// Limiter mais permissivo para endpoints apenas de leitura
const readLimiter = rateLimit({
windowMs: 60 * 1000,
max: 1000
});
// Aplicar às rotas
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
app.get('/api/public/*', readLimiter);
app.use('/api', apiLimiter);

Rate Limiting em Laravel

Limitação Baseada em Rotas

// routes/api.php
Route::middleware('throttle:60,1')->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::get('/posts', [PostController::class, 'index']);
});
// Limite restrito para autenticação
Route::middleware('throttle:5,1')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
});

Rate Limiters Personalizados

// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot()
{
$this->configureRateLimiting();
}
protected function configureRateLimiting()
{
// Limiter padrão da API
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip());
});
// Limites por níveis com base no plano
RateLimiter::for('api-tiered', function (Request $request) {
$user = $request->user();
if (!$user) {
return Limit::perMinute(10)->by($request->ip());
}
return match($user->plan) {
'enterprise' => Limit::none(),
'pro' => Limit::perMinute(1000)->by($user->id),
'basic' => Limit::perMinute(100)->by($user->id),
default => Limit::perMinute(30)->by($user->id),
};
});
// Vários limites
RateLimiter::for('uploads', function (Request $request) {
return [
Limit::perMinute(10), // 10 por minuto
Limit::perHour(100), // 100 por hora
Limit::perDay(500), // 500 por dia
];
});
}

Aplicar Limiters Personalizados

Route::middleware('throttle:api-tiered')->group(function () {
Route::apiResource('users', UserController::class);
});
Route::middleware('throttle:uploads')->group(function () {
Route::post('/upload', [UploadController::class, 'store']);
});

Rate Limiting em Nginx

http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# Status code for rate limited requests
limit_req_status 429;
server {
location /api/ {
# Allow burst of 20, delay after 10
limit_req zone=api burst=20 delay=10;
proxy_pass http://backend;
}
location /api/auth/login {
# Strict: no burst, immediate rejection
limit_req zone=login burst=5 nodelay;
proxy_pass http://backend;
}
}
}

Cabeçalhos de Resposta

Cabeçalhos padrão a incluir:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000
Retry-After: 60
// Middleware Express para adicionar cabeçalhos
function rateLimitHeaders(req, res, next) {
const limit = 100;
const remaining = req.rateLimit?.remaining ?? limit;
const resetTime = req.rateLimit?.resetTime ?? Date.now() + 60000;
res.set({
'X-RateLimit-Limit': limit,
'X-RateLimit-Remaining': remaining,
'X-RateLimit-Reset': Math.ceil(resetTime / 1000)
});
next();
}

Tratamento do Lado do Cliente

async function apiCall(url, options = {}) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
// Backoff exponencial
await new Promise(r => setTimeout(r, retryAfter * 1000));
return apiCall(url, options); // Repetir tentativa
}
// Registar a quota restante
const remaining = response.headers.get('X-RateLimit-Remaining');
if (remaining && parseInt(remaining) < 10) {
console.warn(`Low API quota: ${remaining} requests remaining`);
}
return response;
}

Boas Práticas

Identificar Utilizadores Corretamente

function getUserIdentifier(req) {
// Preferir o ID do utilizador para utilizadores autenticados
if (req.user?.id) {
return `user:${req.user.id}`;
}
// Usar a chave de API se for fornecida
const apiKey = req.headers['x-api-key'];
if (apiKey) {
return `apikey:${apiKey}`;
}
// Recorrer ao IP (considerar X-Forwarded-For atrás de proxy)
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
return `ip:${ip}`;
}

Limites Diferentes para Operações Diferentes

const rateLimits = {
'GET /api/users': { limit: 1000, window: 60 },
'POST /api/users': { limit: 10, window: 60 },
'DELETE /api/users': { limit: 5, window: 60 },
'POST /api/auth/login': { limit: 5, window: 900 },
'POST /api/upload': { limit: 10, window: 3600 }
};

Degradação Elegante

async function handleRequest(req, res) {
const { allowed, remaining } = await checkRateLimit(req);
if (!allowed) {
// Devolver a resposta em cache, se disponível
const cached = await cache.get(req.url);
if (cached) {
res.set('X-Served-From-Cache', 'true');
return res.json(cached);
}
return res.status(429).json({
error: 'Rate limit exceeded',
message: 'Please try again later'
});
}
// Processar normalmente
}

Principais Conclusões

  1. Escolha o algoritmo certo: Token bucket para APIs com tolerância a picos
  2. Use Redis em sistemas distribuídos: Partilhe o estado entre instâncias
  3. Devolva os cabeçalhos corretos: Ajude os clientes a gerir a sua quota
  4. Defina níveis por utilizador/plano: Limites diferentes para clientes diferentes
  5. Combine com Nginx: Proteção dupla ao nível da aplicação e do proxy
  6. Registe e monitorize: Acompanhe ocorrências de rate limit para planeamento de capacidade

O rate limiting é um equilíbrio entre proteção e experiência do utilizador — demasiado restritivo frustra os utilizadores; demasiado permissivo expõe vulnerabilidades. Comece de forma conservadora e ajuste com base em padrões reais de utilização.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive