Webhooks erklärt: Signierte Payloads und Retries
Warum Webhooks knifflig sind
Webhooks sind eine einfache Idee: Statt eine API auf Änderungen zu pollen, sendet der Anbieter Ereignisse per HTTP POST an Ihren Endpunkt. In der Praxis bringen Webhooks jedoch eine Reihe von Herausforderungen mit sich, über die viele Entwickler stolpern: die Überprüfung, ob die Anfrage tatsächlich vom Anbieter stammt, der Umgang mit doppelten Zustellungen und Netzwerkfehlern. Wenn Sie diese ignorieren, riskieren Sie Sicherheitslücken, Datenbeschädigung und verärgerte Nutzer.
Dieser Artikel behandelt die zwei kritischsten Aspekte produktionsreifer Webhooks: signierte Payloads und Retries. Wir erklären, wie Sie Signaturen verifizieren, wie Sie Retries mit exponentiellem Backoff implementieren und wie Sie Ihre Verarbeitung idempotent gestalten.
Signierte Payloads: Authentizität beweisen
Jeder kann einen HTTP POST an Ihren Endpunkt senden. Um sicherzustellen, dass die Anfrage von Ihrem Anbieter stammt, signiert der Anbieter die Payload mit einem geheimen Schlüssel und sendet die Signatur in einem Header. Ihre Aufgabe ist es, die Signatur neu zu berechnen und zu vergleichen.
Die meisten Anbieter verwenden HMAC-SHA256. Die Signatur wird typischerweise über den rohen Request-Body (manchmal mit einem Zeitstempel) berechnet und hex-kodiert. Beispielsweise verwendet Stripe einen Header wie Stripe-Signature, der einen Zeitstempel und eine Signatur enthält.
Wie HMAC funktioniert
HMAC (Hash-based Message Authentication Code) kombiniert einen geheimen Schlüssel mit einer Hash-Funktion. Bei gleichem Schlüssel und gleicher Nachricht ist die Ausgabe deterministisch. Ohne den Schlüssel kann ein Angreifer keine gültige Signatur fälschen.
Hier ist ein minimales Beispiel in Node.js:
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
In Python ist es genauso einfach:
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Best Practices für die Signaturverifikation
- Verwenden Sie zeitkonstanten Vergleich, um Timing-Angriffe zu verhindern. Sowohl
crypto.timingSafeEqualals auchhmac.compare_digestsind sicher. - Verifizieren Sie den rohen Body, nicht ein geparstes JSON-Objekt. Parsen und erneutes Serialisieren kann Leerzeichen ändern und die Signatur ungültig machen.
- Fügen Sie einen Zeitstempel in die signierten Daten ein, um Replay-Angriffe zu verhindern. Lehnen Sie Anfragen ab, die älter als ein paar Minuten sind.
- Speichern Sie Geheimnisse sicher – verwenden Sie Umgebungsvariablen oder einen Secrets Manager, niemals hardcodiert.
Umgang mit Zeitstempeln und Replay-Angriffen
Viele Anbieter fügen einen Zeitstempel in den Signatur-Header ein. Zum Beispiel könnte der Header t=1614556800,v1=abc123... lauten. Sie sollten:
- Den Zeitstempel und die Signatur extrahieren.
- Prüfen, ob der Zeitstempel innerhalb eines akzeptablen Fensters liegt (z. B. 5 Minuten).
- Die Signatur unter Verwendung des Zeitstempels und der Payload neu berechnen.
- Signaturen sicher vergleichen.
Dies verhindert, dass ein Angreifer eine alte gültige Anfrage erneut sendet.
Retries: Fehler elegant behandeln
Webhooks werden über das öffentliche Internet zugestellt, daher sind Fehler unvermeidlich. Ihr Endpunkt könnte ausgefallen, langsam sein oder einen 500-Fehler zurückgeben. Ein robuster Anbieter wird fehlgeschlagene Zustellungen wiederholen, aber Sie müssen auch auf Ihrer Seite Retries handhaben, wenn Sie der Anbieter sind.
Exponentielles Backoff mit Jitter
Vermeiden Sie bei Wiederholungen, den Endpunkt mit sofortigen Retries zu bombardieren. Verwenden Sie exponentielles Backoff: Warten Sie nach jedem Fehler länger. Fügen Sie Jitter (Zufälligkeit) hinzu, um Thundering-Herd-Probleme zu vermeiden, wenn viele Webhooks gleichzeitig fehlschlagen.
Ein typischer Retry-Zeitplan könnte so aussehen:
| Versuch | Verzögerung (ca.) |
|---|---|
| 1 | Sofort |
| 2 | 30 Sekunden |
| 3 | 5 Minuten |
| 4 | 30 Minuten |
| 5 | 2 Stunden |
Nach einer maximalen Anzahl von Versuchen markieren Sie die Zustellung als fehlgeschlagen und alarmieren.
Idempotenz: Der Schlüssel zu sicheren Retries
Da Retries zu doppelten Zustellungen führen können, muss Ihr Webhook-Handler idempotent sein. Das bedeutet, dass die mehrfache Verarbeitung desselben Ereignisses denselben Effekt haben sollte wie die einmalige Verarbeitung.
So erreichen Sie Idempotenz:
- Verwenden Sie eine eindeutige Ereignis-ID, die in der Payload bereitgestellt wird (z. B. das Feld
id). Speichern Sie verarbeitete IDs in einer Datenbank mit einer Unique-Constraint. - Gestalten Sie Operationen idempotent – verwenden Sie beispielsweise Upserts anstelle von Inserts oder prüfen Sie, ob eine Ressource bereits existiert, bevor Sie sie erstellen.
- Geben Sie schnell 2xx zurück und verarbeiten Sie asynchron, wenn die Arbeit umfangreich ist. Dies reduziert die Wahrscheinlichkeit von Timeouts, die Retries auslösen.
Implementierung von Retries auf der Anbieterseite
Wenn Sie ein Webhook-System entwickeln, benötigen Sie einen zuverlässigen Zustellmechanismus. Hier ist ein vereinfachter Ansatz:
- Wenn ein Ereignis eintritt, reihen Sie einen Zustelljob mit der Payload und der Ziel-URL in eine Warteschlange ein.
- Ein Worker nimmt den Job auf und sendet den HTTP POST mit einem Timeout (z. B. 10 Sekunden).
- Wenn der Antwortstatus nicht 2xx ist, planen Sie einen Retry mit exponentiellem Backoff.
- Verfolgen Sie Versuche und geben Sie nach einem Maximum (z. B. 5 Versuche) auf.
- Protokollieren Sie Fehler und bieten Sie eine Möglichkeit für manuelles Wiederholen.
Verwenden Sie eine persistente Warteschlange (z. B. Redis, RabbitMQ oder eine Datenbanktabelle), damit Retries Neustarts überleben.
Alles zusammenfügen: Eine Checkliste für Webhook-Empfänger
Befolgen Sie beim Erstellen eines Webhook-Empfängers diese Checkliste:
- Verifizieren Sie die Signatur mit dem Geheimnis des Anbieters und zeitkonstantem Vergleich.
- Prüfen Sie den Zeitstempel, um Replay-Angriffe zu verhindern.
- Antworten Sie schnell mit 2xx – bestätigen Sie den Empfang, bevor Sie mit der schweren Verarbeitung beginnen.
- Verarbeiten Sie asynchron, falls nötig, mithilfe einer Warteschlange.
- Stellen Sie Idempotenz sicher, indem Sie Ereignis-IDs verfolgen.
- Protokollieren Sie alles für Debugging und Auditing.
- Überwachen Sie Fehler und richten Sie Alarme für wiederholte Retries ein.
FAQ
Was ist eine signierte Webhook-Payload?
Eine signierte Payload enthält eine kryptografische Signatur (normalerweise HMAC-SHA256), die mit einem gemeinsamen Geheimnis generiert wird. Der Empfänger berechnet die Signatur neu, um zu verifizieren, dass die Anfrage vom erwarteten Absender stammt und nicht manipuliert wurde.
Warum benötigen Webhooks Retries?
Netzwerkprobleme, Serverausfälle oder vorübergehende Fehler können dazu führen, dass eine Webhook-Zustellung fehlschlägt. Retries mit exponentiellem Backoff stellen sicher, dass das Ereignis schließlich zugestellt wird, ohne den Empfänger zu überlasten.
Wie verhindere ich doppelte Webhook-Verarbeitung?
Verwenden Sie Idempotenz: Fügen Sie eine eindeutige Ereignis-ID in die Payload ein, speichern Sie verarbeitete IDs und gestalten Sie Ihre Operationen idempotent (z. B. Upserts). So ist der Effekt selbst dann gleich, wenn ein Webhook mehrfach zugestellt wird.
Wenn Sie Webhook-Payloads inspizieren oder debuggen müssen, kann Ihnen der JSON Formatter helfen, JSON-Daten schnell übersichtlich zu formatieren und zu validieren.