Webhooks erklärt: Signierte Payloads und Retries
Du hast gerade einen Zahlungsanbieter eines Drittanbieters integriert. Der Nutzer schließt einen Kauf ab, aber deine Datenbank wird nie aktualisiert. Warum? Weil der Webhook, der deine App benachrichtigen sollte, stillschweigend fehlgeschlagen ist. Ohne ordnungsgemäße Signaturprüfung und Retry-Logik können Webhooks zu einer Quelle mysteriöser Bugs und Sicherheitslücken werden.
In diesem Artikel erklären wir, wie Webhooks funktionieren, warum du signierte Payloads verifizieren musst und wie du einen robusten Retry-Mechanismus entwirfst. Außerdem zeigen wir dir, wie du Webhooks effektiv testen und debuggen kannst.
Was sind Webhooks?
Webhooks sind benutzerdefinierte HTTP-Callbacks. Sie ermöglichen es einem System, einem anderen in Echtzeit Daten zu senden, wenn ein Ereignis eintritt. Wenn beispielsweise eine Zahlung verarbeitet wird, sendet der Zahlungsanbieter eine HTTP-POST-Anfrage an eine von dir angegebene URL, die Details zum Ereignis enthält.
Anders als beim Polling, bei dem deine App wiederholt nach Updates fragt, pushen Webhooks Daten zu dir, sobald sie verfügbar sind. Das macht sie effizient und nahezu in Echtzeit.
Das Sicherheitsproblem: Warum Signaturen verifizieren?
Webhook-Endpunkte sind öffentlich zugängliche URLs. Jeder kann eine POST-Anfrage an sie senden. Ohne Verifikation könnte ein Angreifer eine Anfrage fälschen, um zum Beispiel eine Bestellung als bezahlt zu markieren, ohne dass tatsächlich eine Zahlung erfolgt ist. Um dies zu verhindern, signieren Anbieter jeden Payload mit einem geheimen Schlüssel.
So funktionieren HMAC-Signaturen
Die meisten Anbieter verwenden HMAC (Hash-based Message Authentication Code) mit SHA-256. Sie berechnen einen Hash des Request-Bodys mit einem gemeinsamen Secret und fügen ihn in einen Header ein, etwa X-Signature. Dein Server berechnet den Hash neu und vergleicht ihn mit dem Header. Wenn sie übereinstimmen, ist die Anfrage authentisch.
Hier ist ein einfaches Beispiel in Python:
import hmac
import hashlib
def verify_signature(payload, secret, signature):
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Verwende immer eine Vergleichsfunktion mit konstanter Laufzeit wie hmac.compare_digest, um Timing-Angriffe zu vermeiden.
Häufige Fehler, die du vermeiden solltest
- Das falsche Secret verwenden: Jede Umgebung (Test, Live) sollte ihr eigenes Secret haben.
- Den Zeitstempel nicht prüfen: Manche Anbieter fügen einen Zeitstempel hinzu, um Replay-Angriffe zu verhindern. Lehne Anfragen ab, die älter als ein paar Minuten sind.
- Secrets loggen: Logge niemals das Secret oder die vollständige Signatur in der Produktion.
Zuverlässige Retries entwerfen
Webhooks werden über das Internet gesendet, daher sind Fehler unvermeidlich. Netzwerke fallen aus, Server starten neu und Endpunkte laufen in Timeouts. Ein gutes Webhook-System wiederholt fehlgeschlagene Zustellungen mit exponentiellem Backoff.
So funktionieren Retries typischerweise
Anbieter wiederholen in der Regel mit zunehmenden Verzögerungen: nach 1 Minute, 5 Minuten, 30 Minuten usw., bis zu einer maximalen Anzahl von Versuchen (z. B. 5–10). Wenn alle Retries fehlschlagen, wird das Ereignis als fehlgeschlagen markiert und steht möglicherweise für ein manuelles Replay zur Verfügung.
Dein Endpunkt sollte schnell mit einem 2xx-Statuscode antworten, um den Empfang zu bestätigen. Wenn du einen Nicht-2xx-Code zurückgibst oder in ein Timeout läufst, wird der Anbieter es erneut versuchen.
Idempotenz implementieren
Da Retries zu doppelten Zustellungen führen können, muss dein Endpunkt idempotent sein. Verwende die Event-ID (enthalten im Payload oder in den Headern), um verarbeitete Ereignisse zu verfolgen. Prüfe vor der Verarbeitung, ob du diese ID bereits behandelt hast. Speichere verarbeitete IDs in einer Datenbank mit TTL.
Beispiel für eine Idempotenz-Prüfung:
def process_event(event_id, data):
if db.exists(f"processed:{event_id}"):
return # already processed
# process data
db.setex(f"processed:{event_id}", 86400, "1") # expire after 24h
Einen Webhook-Empfänger bauen: Schritt für Schritt
- Stelle einen HTTPS-Endpunkt bereit: Verwende TLS, um Daten während der Übertragung zu schützen.
- Verifiziere die Signatur: Lehne Anfragen mit ungültigen Signaturen mit einem 401 ab.
- Prüfe den Zeitstempel: Falls vorhanden, stelle sicher, dass er innerhalb eines akzeptablen Zeitfensters liegt (z. B. 5 Minuten).
- Parse den Payload: Validiere die JSON-Struktur und die erforderlichen Felder.
- Prüfe die Idempotenz: Verwende die Event-ID, um Duplikate zu vermeiden.
- Verarbeite asynchron: Antworte sofort mit 200 und verarbeite dann in einem Hintergrundjob, um Timeouts zu vermeiden.
- Logge und überwache: Erfasse Fehler und richte Alerts ein.
Webhooks lokal testen
Während der Entwicklung brauchst du eine Möglichkeit, Webhooks auf deinem lokalen Rechner zu empfangen. Tools wie ngrok oder localtunnel erstellen eine öffentliche URL, die an dein localhost weiterleitet. Alternativ bieten viele Anbieter eine CLI an, um Testereignisse auszulösen.
Teste die Signaturprüfung immer sowohl mit gültigen als auch mit ungültigen Signaturen. Simuliere Retries, indem du Fehlercodes zurückgibst und das Retry-Verhalten des Anbieters beobachtest.
Vergleich: Webhooks vs. Polling
| Aspekt | Webhooks | Polling |
|---|---|---|
| Echtzeit | Nahezu in Echtzeit | Verzögert um das Intervall |
| Effizienz | Sendet Daten nur, wenn ein Ereignis eintritt | Häufige Anfragen auch ohne Daten |
| Komplexität | Erfordert Endpunkt, Signaturprüfung, Retries | Einfacher zu implementieren |
| Zuverlässigkeit | Hängt vom Retry-Mechanismus ab | Du kontrollierst den Zeitplan |
FAQ
Was ist ein signierter Webhook-Payload?
Ein signierter Payload enthält eine kryptografische Signatur (üblicherweise HMAC), die es dem Empfänger ermöglicht, zu verifizieren, dass die Anfrage vom erwarteten Absender stammt und nicht manipuliert wurde.
Wie oft sollte ein Webhook wiederholt werden?
Die meisten Anbieter wiederholen 5–10 Mal mit exponentiellem Backoff. Die genaue Anzahl hängt von der Richtlinie des Anbieters ab. Dein Endpunkt sollte idempotent sein, um Duplikate zu verarbeiten.
Welchen HTTP-Statuscode sollte mein Webhook zurückgeben?
Gib einen 2xx-Statuscode zurück (z. B. 200 OK), um den erfolgreichen Empfang zu bestätigen. Jeder andere Code (4xx oder 5xx) signalisiert einen Fehler und löst Retries aus.
Bereit, deine Webhook-Payloads zu testen? Verwende unseren JSON Formatter, um JSON-Payloads schnell zu validieren und übersichtlich zu formatieren.