Επαλήθευση υπογραφών webhook
Κάθε αίτημα που στέλνουμε στον διακομιστή σας — παραδόσεις webhook και επικλήσεις τελικών σημείων εργαλείων — φέρει υπογραφή HMAC-SHA256 στην κεφαλίδα X-ThunderPhone-Signature. Ρυθμίστε σωστά την επαλήθευση μία φορά και χρησιμοποιήστε τον ίδιο βοηθό σε κάθε χειριστή.
Ο αλγόριθμος
- Διαβάστε το ακατέργαστο σώμα του αιτήματος — τα ακριβή byte που σας στείλαμε με POST.
- Υπολογίστε
hmac_sha256(secret, body).hexdigest(). - Συγκρίνετε σε σταθερό χρόνο με το
X-ThunderPhone-Signature. (Η απλή σύγκριση συμβολοσειρών διαρρέει πληροφορίες χρονισμού.)
Υπογράφουμε ακριβώς τα byte που μεταδίδουμε, επομένως η επαλήθευση του ακατέργαστου σώματος λειτουργεί πάντα. Αυτά τα byte είναι επίσης η κανονική σειριοποίηση JSON του ωφέλιμου φορτίου — κλειδιά ταξινομημένα αλφαβητικά, συμπαγείς διαχωριστές
(, και : χωρίς κενά), UTF-8. Αυτό σας παρέχει μια δεύτερη, πλήρως
ισοδύναμη μέθοδο όταν το framework σας εκθέτει μόνο αναλυμένο JSON:
επανασειριοποιήστε κανονικά και υπολογίστε το HMAC αυτού.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
Προτιμήστε το ακατέργαστο σώμα — είναι ένα βήμα λιγότερο και δεν επηρεάζεται από ιδιαιτερότητες επαναμετατροπής αριθμών JSON σε ορισμένες γλώσσες.
Ποιο μυστικό;
| Πηγή | Μυστικό |
|---|---|
Τελικό σημείο webhook (/v1/developer/webhook-endpoints) | secret ανά τελικό σημείο (48 δεκαεξαδικοί χαρακτήρες) που επιστρέφεται μία φορά κατά τη δημιουργία |
| Παλιό webhook μίας διεύθυνσης URL | secret ανά οργανισμό που επιστρέφεται από το GET /v1/webhook |
Επίκληση τελικού σημείου εργαλείου (άμεση κλήση στο endpoint.url σας) | Το μυστικό webhook σε επίπεδο οργανισμού (το ίδιο με εκείνο του παλιού webhook μίας διεύθυνσης URL) — όχι μυστικό ανά τελικό σημείο |
Αποθηκεύστε το μυστικό στον διαχειριστή μυστικών σας ή σε μεταβλητή περιβάλλοντος — μην το υποβάλετε ποτέ σε commit.
Υλοποιήσεις αναφοράς
Και οι τέσσερις επαληθεύουν το ακατέργαστο σώμα του αιτήματος:
import hashlib
import hmac
def verify(body: bytes, signature: str, secret: str) -> bool:
"""Constant-time HMAC-SHA256 verification."""
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature or "")
import crypto from "node:crypto";
export function verify(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (!signature || expected.length !== signature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func Verify(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
require "openssl"
def verify(body, signature, secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
Rack::Utils.secure_compare(expected, signature.to_s)
end
Σύνδεση ανά framework
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
@app.post("/thunderphone-webhook")
async def hook(request: Request):
body = await request.body() # raw bytes, NOT request.json()
sig = request.headers.get("X-ThunderPhone-Signature", "")
if not verify(body, sig, SECRET):
raise HTTPException(status_code=401)
import json
event = json.loads(body)
# … dispatch on event["type"] …
return {"ok": True}
import express from "express";
const app = express();
app.post(
"/thunderphone-webhook",
// IMPORTANT: parse as raw; do NOT use express.json() here.
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("X-ThunderPhone-Signature") || "";
if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// … dispatch on event.type …
res.sendStatus(204);
},
);
import json
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def hook(request):
body = request.body # raw bytes
sig = request.headers.get("X-ThunderPhone-Signature", "")
if not verify(body, sig, SECRET):
return HttpResponseForbidden("invalid signature")
event = json.loads(body)
# … dispatch on event["type"] …
return JsonResponse({"ok": True})
Επαλήθευση κλήσεων εργαλείων
Όταν ο πράκτορας καλεί απευθείας ένα από τα
εργαλεία συναρτήσεων σας (το εργαλείο διαθέτει ένα
endpoint), το αίτημα περιλαμβάνει δύο κεφαλίδες ThunderPhone μαζί με
τις ρυθμισμένες endpoint.headers σας:
X-ThunderPhone-Call-ID— το αριθμητικό αναγνωριστικό της ενεργής κλήσης.X-ThunderPhone-Signature— HMAC-SHA256, με κλειδί το μυστικό webhook σε επίπεδο οργανισμού, στα ακριβή byte του σώματος του αιτήματος.
Ο ίδιος βοηθός verify() λειτουργεί χωρίς αλλαγές, με δύο ιδιαιτερότητες:
- Τα εργαλεία
GET/DELETEδεν έχουν σώμα. Τα ορίσματα μεταφέρονται ως παράμετροι ερωτήματος και η υπογραφή υπολογίζεται πάνω στην κενή συμβολοσειρά byte — επομένωςverify(b"", sig, secret)(Python) ήverify(Buffer.alloc(0), sig, secret)(Node). Μην κάνετε hash τη συμβολοσειρά ερωτήματος. - Οι οργανισμοί χωρίς ρυθμισμένο παλαιό webhook δεν έχουν μυστικό οργανισμού. Σε αυτή την περίπτωση, οι κλήσεις εργαλείων περιλαμβάνουν μόνο το
X-ThunderPhone-Call-IDκαι καμία κεφαλίδα υπογραφής. Ρυθμίστε το παλαιό webhook (PUT /v1/webhook) για να λάβετε μυστικό υπογραφής ή πραγματοποιήστε έλεγχο ταυτότητας των κλήσεων εργαλείων με τη δική σας κεφαλίδα μέσω τουendpoint.headers.
@app.post("/tools/search-appointments")
async def tool(request: Request):
body = await request.body() # b"" for GET/DELETE tools
sig = request.headers.get("X-ThunderPhone-Signature", "")
call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
if not verify(body, sig, ORG_WEBHOOK_SECRET):
raise HTTPException(status_code=401)
args = json.loads(body)
...
Η αποστολή εργαλείων σε λειτουργία webhook (εργαλεία χωρίς endpoint, που παραδίδονται στο webhook του οργανισμού σας ως telephony.tool / web.tool) είναι ένα συνηθισμένο υπογεγραμμένο webhook — ισχύει η τυπική διαδικασία παραπάνω. Δείτε τα
Εργαλεία συναρτήσεων για τις δύο μορφές αιτημάτων.
Συνήθη προβλήματα
Επανασειριοποίηση με προεπιλεγμένη μορφοποίηση
Η ανάλυση του σώματος και η επαναεξαγωγή του με τις
προεπιλογές της βιβλιοθήκης JSON σας (κενά μετά από , / :, κλειδιά με σειρά εισαγωγής) παράγει
διαφορετικά byte και καταστρέφει το HMAC. Επαληθεύστε το ακατέργαστο σώμα — ή, αν
πρέπει να το επανασειριοποιήσετε, αντιστοιχίστε ακριβώς την κανονική μορφή μας:
ταξινομημένα κλειδιά, συμπαγείς διαχωριστές, UTF-8.
Το framework αναλύει αυτόματα το JSON
Το middleware express.json() του Express καταναλώνει τη ροή του σώματος
και χάνετε τα ακατέργαστα byte. Χρησιμοποιήστε το express.raw() ειδικά στη διαδρομή webhook
ή αποθηκεύστε προσωρινά το ακατέργαστο σώμα σε ένα προ-middleware.
Το ίδιο ισχύει για NestJS / Koa — ελέγξτε την τεκμηρίωσή τους για το «ακατέργαστο σώμα».
Σύγκριση μη ασφαλής ως προς τον χρόνο
Το expected === signature σε JS ή το expected == signature σε
Python είναι συγκρίσεις μεταβλητού χρόνου. Χρησιμοποιήστε αντίστοιχα το crypto.timingSafeEqual
ή το hmac.compare_digest. Η διαφορά στην απόδοση
είναι μηδενική.
Λανθασμένο μυστικό για endpoint εργαλείων
Οι απευθείας κλήσεις σε endpoint εργαλείων υπογράφονται με το μυστικό webhook
σε επίπεδο οργανισμού (GET /v1/webhook) — όχι με οποιοδήποτε μυστικό ανά endpoint
από το /v1/developer/webhook-endpoints. Επαναχρησιμοποιήστε την ίδια συνάρτηση verify(),
αλλά βεβαιωθείτε ότι τροφοδοτείτε με το μυστικό οργανισμού τις διαδρομές εργαλείων.
Κατακερματισμός του query string σε εργαλεία GET/DELETE
Για μεθόδους εργαλείων χωρίς σώμα, η υπογραφή καλύπτει την κενή ακολουθία byte, διατηρώντας μία καθολική συνταγή: εφαρμόστε HMAC στο ακατέργαστο σώμα του αιτήματος, όποιο κι αν είναι. Ο κατακερματισμός του URL ή του query string δεν θα αντιστοιχεί ποτέ.
Μη επιστροφή 401 σε ασυμφωνία
Η επιστροφή 200 σε αποτυχημένη επαλήθευση καθιστά τον χειριστή στόχο επανάληψης αιτημάτων. Να απαντάτε πάντα με μη-2xx αν η επαλήθευση αποτύχει.
Επόμενα βήματα
Σημασιολογία παράδοσης, επαναλήψεις, IP προέλευσης.
Διαχειριστείτε πολλαπλά URL, εναλλάξτε μυστικά.
Οι δύο διαδρομές επίκλησης εργαλείων και οι μορφές των αιτημάτων τους.
Δημιουργήστε μια ολοκληρωμένη ενσωμάτωση με υποστήριξη εργαλείων από άκρο σε άκρο.