Webhooks erklärt: Signierte Payloads und Retries
Du hast gerade ein Zahlungsgateway integriert und musst nun deine Datenbank aktualisieren, wenn eine Zahlung erfolgreich ist. Die API alle paar Sekunden zu pollen ist verschwenderisch und langsam. Webhooks lösen dies, indem sie Events an deinen Server pushen, sobald sie auftreten. Aber ohne ordnungsgemäße Sicherheit und Zuverlässigkeit können Webhooks zu einer Quelle von Bugs und Sicherheitslücken werden. Dieser Artikel erklärt, wie du Webhooks korrekt implementierst, mit Fokus auf signierte Payloads und Retries.
Was sind Webhooks?
Ein Webhook ist ein HTTP-Callback: Ein Event tritt in einem Dienst auf, und dieser Dienst sendet eine HTTP-POST-Anfrage an eine URL, die du bereitstellst. Der Payload enthält typischerweise JSON-Daten über das Event. Wenn zum Beispiel ein Kunde eine Zahlung abschließt, sendet Stripe ein charge.succeeded-Event an deinen Endpoint.
Webhooks sind push-basiert und reduzieren daher die Latenz und die Serverlast im Vergleich zum Polling. Sie bringen jedoch Herausforderungen mit sich: Woher weißt du, dass die Anfrage echt ist? Was, wenn dein Server down ist, wenn das Event ausgelöst wird?
Warum signierte Payloads wichtig sind
Jeder kann eine HTTP-POST-Anfrage an deinen Webhook-Endpoint senden. Ohne Verifizierung könnte ein Angreifer Events fälschen, was zu unbefugten Aktionen wie dem Markieren einer Bestellung als bezahlt führen könnte. Signierte Payloads lösen dies, indem sie eine kryptografische Signatur enthalten, die nur der Absender und du generieren können.
Die gebräuchlichste Methode ist HMAC (Hash-based Message Authentication Code) mit einem gemeinsamen Secret. Der Absender berechnet einen Hash des Payloads mit dem Secret und fügt ihn in einen Header ein. Du berechnest den Hash neu und vergleichst. Wenn sie übereinstimmen, ist die Anfrage authentisch.
So verifizierst du einen signierten Payload
Hier ist ein schrittweiser Ansatz mit Node.js und Express:
- Rufe den rohen Request-Body als String ab. Parse ihn nicht als JSON vor der Verifizierung, da das Parsen die Byte-Sequenz verändern kann.
- Extrahiere die Signatur aus dem Header (z. B.
X-Signature). - Berechne den HMAC mit deinem Secret und dem rohen Body.
- Vergleiche die berechnete Signatur mit der empfangenen unter Verwendung eines Constant-Time-Vergleichs, um Timing-Angriffe zu verhindern.
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.status(200).send('OK');
});
Verwende immer ein starkes Secret und rotiere es regelmäßig. Gib es niemals in clientseitigem Code preis.
Retries mit Backoff implementieren
Webhooks werden über das Internet zugestellt, daher treten Fehler auf. Dein Server könnte down sein, oder das Netzwerk könnte stocken. Ein robustes Webhook-System wiederholt fehlgeschlagene Zustellungen mit exponentiellem Backoff.
Typischerweise wiederholt der Absender den Versuch, wenn er nicht innerhalb eines Timeouts (z. B. 5 Sekunden) eine 2xx-Antwort erhält. Der Retry-Zeitplan könnte sein: sofort, dann nach 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden usw., bis zu einer maximalen Anzahl von Versuchen (z. B. 5).
Als Empfänger musst du schnell antworten, um Timeouts zu vermeiden. Bestätige den Empfang mit einem 200 OK und verarbeite das Event asynchron.
Idempotenz handhaben
Da Retries zu doppelten Zustellungen führen können, muss deine Event-Verarbeitung idempotent sein. Füge eine eindeutige Event-ID in den Payload ein und speichere verarbeitete IDs. Prüfe vor der Verarbeitung, ob die ID bereits behandelt wurde.
async function processEvent(event) {
const { id, type, data } = event;
if (await db.processedEvents.findOne({ id })) {
return; // Already processed
}
await db.processedEvents.insertOne({ id });
// Handle event based on type
}
Verwende eine Datenbanktransaktion, um sicherzustellen, dass das Event erst nach erfolgreicher Verarbeitung als verarbeitet markiert wird.
Best Practices für Webhook-Sicherheit
- Verifiziere immer Signaturen vor der Verarbeitung.
- Verwende HTTPS, um Abhören zu verhindern.
- Validiere den Zeitstempel, falls enthalten, um Replay-Angriffe zu verhindern.
- Begrenze die Payload-Größe, um Denial-of-Service zu vermeiden.
- Logge alle Webhook-Versuche für Debugging und Auditing.
Vergleich: Polling vs. Webhooks
| Aspekt | Polling | Webhooks |
|---|---|---|
| Latenz | Hoch (intervallbasiert) | Niedrig (nahezu Echtzeit) |
| Serverlast | Hoch (konstante Anfragen) | Niedrig (nur bei Events) |
| Komplexität | Einfach zu implementieren | Erfordert Sicherheit & Retries |
| Zuverlässigkeit | Abhängig von Polling-Frequenz | Abhängig von Retry-Logik |
FAQ
Wie teste ich Webhooks lokal?
Verwende ein Tool wie ngrok, um deinen lokalen Server im Internet verfügbar zu machen. Konfiguriere den Webhook-Anbieter so, dass er Events an deine ngrok-URL sendet.
Was, wenn mein Server down ist, wenn ein Webhook gesendet wird?
Der Absender sollte mit Backoff wiederholen. Stelle sicher, dass dein Server hochverfügbar ist und schnell antwortet, um Timeouts zu vermeiden.
Kann ich asymmetrische Signaturen anstelle von HMAC verwenden?
Ja, einige Anbieter verwenden RSA- oder ECDSA-Signaturen. Du verifizierst mit ihrem öffentlichen Schlüssel. HMAC ist einfacher, erfordert aber ein gemeinsames Secret.
Beim Debuggen von Webhook-Payloads ist es hilfreich, das JSON für bessere Lesbarkeit zu formatieren. Probiere unseren JSON Formatter, um Webhook-Payloads sofort hübsch zu formatieren und zu validieren.