Webhooks Explicados: Payloads Assinados e Lógica de Retry
Você construiu uma API e agora precisa notificar outros serviços quando eventos acontecem. Você decide implementar webhooks: callbacks HTTP que enviam dados para URLs de assinantes. Mas logo você enfrenta duas questões críticas: Como garantir que o payload não foi adulterado? E o que acontece quando o servidor receptor está fora do ar ou retorna um erro? Sem as devidas salvaguardas, webhooks podem se tornar uma brecha de segurança e um pesadelo de confiabilidade.
Neste guia, vamos percorrer os fundamentos para proteger webhooks com payloads assinados e implementar lógica de retry robusta. Você aprenderá como gerar e verificar assinaturas HMAC, projetar handlers idempotentes e construir um mecanismo de retry que lida com falhas graciosamente. Ao final, você terá uma abordagem pronta para produção para webhooks.
Por que Webhooks Precisam de Segurança e Confiabilidade
Webhooks são essencialmente requisições HTTP server-to-server disparadas por eventos. Diferente de chamadas de API iniciadas por um cliente, webhooks são enviados do seu servidor para o endpoint de um assinante. Esse modelo de push introduz dois desafios principais:
- Segurança: O payload trafega pela internet e pode ser interceptado ou falsificado. Os assinantes devem verificar que a requisição realmente veio do seu serviço.
- Confiabilidade: O servidor do assinante pode estar temporariamente indisponível, lento ou retornar um erro. Você precisa de uma estratégia para tentar novamente entregas falhas sem causar processamento duplicado.
Ignorar esses desafios pode levar a vazamentos de dados, perda de eventos ou estado inconsistente entre sistemas. Vamos abordá-los um por um.
Protegendo Webhooks com Payloads Assinados
Um payload assinado usa um hash criptográfico para provar a integridade e autenticidade da requisição. A abordagem mais comum é HMAC (Hash-based Message Authentication Code) com um segredo compartilhado. Veja como funciona:
- Segredo compartilhado: Tanto o remetente (seu serviço) quanto o receptor (assinante) compartilham uma chave secreta. Essa chave é gerada quando o assinante registra seu endpoint de webhook.
- Geração da assinatura: Para cada requisição de webhook, o remetente calcula um HMAC do corpo da requisição (e às vezes cabeçalhos como timestamp) usando o segredo. A assinatura é enviada em um cabeçalho, por exemplo,
X-Signature. - Verificação da assinatura: O receptor recalcula o HMAC usando o mesmo segredo e compara com a assinatura recebida. Se corresponderem, o payload é autêntico e não foi modificado.
Vamos ver um exemplo de código em Node.js para gerar e verificar assinaturas.
const crypto = require('crypto');
// Remetente: gera assinatura
function generateSignature(payload, secret) {
return crypto.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
}
// Receptor: verifica assinatura
function verifySignature(payload, secret, receivedSignature) {
const expectedSignature = generateSignature(payload, secret);
// Use timingSafeEqual para prevenir ataques de temporização
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
Importante: Sempre use uma função de comparação de tempo constante como timingSafeEqual para evitar ataques de temporização. Além disso, inclua um timestamp nos dados assinados para prevenir ataques de replay. Por exemplo, assine timestamp + '.' + payload e rejeite requisições mais antigas que alguns minutos.
Melhores Práticas para Payloads Assinados
- Use HMAC-SHA256 ou mais forte.
- Inclua um timestamp na assinatura e imponha uma idade máxima (por exemplo, 5 minutos).
- Nunca transmita o segredo na requisição; ele é usado apenas para assinatura.
- Rotacione segredos periodicamente e suporte múltiplos segredos ativos durante a rotação.
Implementando Lógica de Retry para Entrega Confiável
Mesmo com a segurança implementada, webhooks podem falhar devido a problemas de rede, erros de servidor ou timeouts. Um mecanismo de retry robusto garante que os eventos sejam eventualmente entregues. Aqui está uma abordagem passo a passo:
- Defina um timeout: Defina um timeout razoável para requisições de webhook (por exemplo, 10 segundos). Se o assinante não responder a tempo, considere uma falha.
- Classifique as respostas: Trate códigos de status 2xx como sucesso. Para erros 4xx (exceto 429), tentar novamente geralmente é inútil porque a requisição é inválida. Para erros 5xx e timeouts, tente novamente com backoff exponencial.
- Backoff exponencial com jitter: Agende novas tentativas em intervalos crescentes (por exemplo, 1s, 5s, 30s, 5m, 30m) e adicione jitter aleatório para evitar problemas de thundering herd.
- Limite as tentativas: Defina um número máximo de tentativas (por exemplo, 5) e então marque a entrega como falha. Opcionalmente, notifique o assinante por e-mail ou painel.
- Idempotência: Como as tentativas podem causar entregas duplicadas, inclua um ID de evento único no payload e no cabeçalho. Os assinantes devem usar esse ID para deduplicar o processamento.
Aqui está uma implementação simplificada de fila de retry em Python usando requests e time:
import requests
import time
import random
def deliver_webhook(url, payload, secret, max_retries=5):
signature = generate_signature(payload, secret)
headers = {'X-Signature': signature, 'Content-Type': 'application/json'}
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
if 200 <= response.status_code < 300:
return True
elif response.status_code == 429 or 500 <= response.status_code < 600:
# Erro que permite retry
pass
else:
# Erro que não permite retry
return False
except requests.exceptions.RequestException:
# Erro de rede, tente novamente
pass
# Backoff exponencial com jitter
delay = min(30, 2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
Idempotência: Lidando com Entregas Duplicadas
Como as tentativas podem fazer com que o mesmo evento seja entregue várias vezes, os assinantes devem implementar processamento idempotente. A abordagem padrão é incluir um ID de evento único (por exemplo, um UUID) no payload e fazer com que o receptor armazene IDs processados por um certo período. Se um ID duplicado for recebido, o receptor o reconhece sem reprocessar.
Aqui está uma verificação simples de idempotência em um handler de webhook:
processed_events = set()
def handle_webhook(event):
event_id = event['id']
if event_id in processed_events:
return 'já processado'
# Processa evento
processed_events.add(event_id)
return 'sucesso'
Em produção, use um armazenamento persistente como Redis ou um banco de dados com TTL para rastrear IDs processados.
Comparação: Payloads Assinados vs. Outras Medidas de Segurança
| Abordagem | Prós | Contras |
|---|---|---|
| Payloads assinados com HMAC | Forte integridade e autenticidade; nenhum segredo transmitido | Requer gerenciamento de segredo compartilhado |
| Basic Auth | Simples de implementar | Credenciais enviadas em cada requisição; menos seguro |
| Lista de permissões de IP | Bloqueia fontes não autorizadas | IPs podem mudar; não adequado para ambientes dinâmicos |
| Credenciais de cliente OAuth2 | Padronizado; baseado em token | Exagerado para webhooks simples; sobrecarga de gerenciamento de token |
Payloads assinados com HMAC oferecem um bom equilíbrio entre segurança e simplicidade para a maioria dos casos de uso de webhooks.
Juntando Tudo
Para construir um sistema de webhook confiável e seguro:
- Gere um segredo único por assinante e assine cada payload com HMAC.
- Inclua um timestamp e ID de evento nos dados assinados.
- Implemente tentativas com backoff exponencial e jitter para falhas transitórias.
- Garanta idempotência no lado do receptor usando IDs de evento.
- Monitore as taxas de sucesso de entrega e alerte sobre falhas.
Ao depurar entregas de webhooks, inspecionar as requisições e respostas HTTP brutas é inestimável. Se você estiver trabalhando com logs do Nginx para rastrear requisições de webhook recebidas, o Analisador de Logs do Nginx pode ajudá-lo a analisar e filtrar entradas de log para identificar entregas falhas ou padrões suspeitos.
Perguntas Frequentes
O que é um payload de webhook assinado?
Um payload de webhook assinado inclui uma assinatura criptográfica (geralmente HMAC) que permite ao receptor verificar a autenticidade e integridade da requisição. O remetente calcula a assinatura usando um segredo compartilhado e a inclui em um cabeçalho; o receptor recalcula e compara.
Quantas vezes devo tentar novamente um webhook falho?
Uma prática comum é de 3 a 5 tentativas com backoff exponencial. Depois disso, marque a entrega como falha e notifique o assinante. O número exato depende do seu SLA e da criticidade do evento.
Por que a idempotência é importante para webhooks?
As tentativas podem causar entregas duplicadas. A idempotência garante que processar o mesmo evento várias vezes tenha o mesmo efeito que processá-lo uma vez, evitando corrupção de dados ou ações duplicadas.