Open in
Επαληθεύστε τις υπογραφές webhook
Κάθε αίτημα webhook και εργαλείου που στέλνει το ThunderPhone είναι υπογεγραμμένο. Επαληθεύστε την υπογραφή μία φορά με την παρακάτω διαδικασία και, στη συνέχεια, χρησιμοποιήστε τον ίδιο έλεγχο σε κάθε τελικό σημείο που εκτελείτε.
Κάθε αίτημα που στέλνουμε στον διακομιστή σας — παραδόσεις 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 αντίστοιχα. Η διαφορά στην απόδοση
είναι μηδενική.
Λανθασμένο μυστικό για endpoints εργαλείων
Οι άμεσες κλήσεις σε 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, εναλλάξτε μυστικά.
Οι δύο διαδρομές επίκλησης εργαλείων και οι μορφές αιτημάτων τους.
Δημιουργήστε μια πλήρη ενσωμάτωση με υποστήριξη εργαλείων από άκρο σε άκρο.