Stripe webhooks în PHP: un handler de încredere în care poți avea încredere
Un webhook Stripe care funcționează pe laptopul tău nu este încă un webhook în care poți avea încredere cu bani. Acest ghid arată cum să verificați semnăturile, să recunoașteți rapid, să deduplicați evenimentele și să gestionați reîncercări în PHP simplu și Laravel, cu cod pe care îl puteți lipi într-un proiect.
Pe scurt
- Tratați webhook, nu redirecționarea browserului, ca sursă de adevăr pentru starea plății.
- Citiți corpul cererii brute și verificați antetul
Stripe-Signatureînainte de a atinge sarcina utilă. - Întoarceți rapid un răspuns 2xx și faceți treaba reală într-o coadă sau după stocarea evenimentului.
- Stocați ID-ul evenimentului cu un index unic: Stripe poate livra același eveniment de mai multe ori și nu este în ordine.
- Testați cu Stripe CLI (
stripe listenșistripe trigger) înainte de a fi difuzat.
Dacă acceptați plăți cu Stripe, webhook-urile Stripe în PHP sunt partea care decide dacă comenzile sunt marcate ca plătite, abonamentele sunt activate și rambursările sunt reflectate în baza de date. Pagina de checkout este doar ușa din față: clienții închid filele, băncile confirmă cu întârziere și unele metode de plată se stabilesc câteva minute sau zile mai târziu. Webhook-ul este mesajul de la Stripe care spune ce sa întâmplat cu adevărat.
Construim și menținem integrări de plată pentru clienți și pentru propriile noastre produse (de exemplu Bill, care preia plățile Stripe și PayPal pe o pagină publică de plată). Acest articol este lista de verificare pe care o urmăm atunci când scriem un punct final Stripe webhook în PHP.
Cum funcționează webhook-urile Stripe într-un minut
Înregistrați o adresă URL HTTPS în Stripe Dashboard (sau prin API) și alegeți ce tipuri de evenimente să primiți. Când se întâmplă ceva - se finalizează o sesiune de plată, se plătește o factură, se rambursează o taxă - Stripe trimite o solicitare POST cu un obiect Eveniment JSON la adresa URL respectivă. Serverul dvs. trebuie să răspundă cu un cod de stare 2xx. În caz contrar, Stripe reîncearcă cu backoff exponențial timp de până la trei zile în modul live.
Trei fapte modelează întregul design:
- Evenimentele pot veni de mai multe ori. Reîncercările și problemele de rețea fac duplicatele normale.
- Evenimentele pot ajunge în dezordine. Este posibil să vedeți
invoice.paidînainte cacustomer.subscription.created. - Oricine poate POSTA la adresa dvs. URL. Fără verificarea semnăturii, un atacator își poate marca propria comandă ca fiind plătită.
Pasul 1. Instalați SDK-ul și creați punctul final
Utilizați biblioteca oficială, stripe/stripe-php și păstrați punctul final secret (începe cu whsec_) într-o variabilă de mediu, niciodată în depozit.
composer require stripe/stripe-php
# .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Fiecare punct final de webhook din Dashboard are propriul său secret de semnare, iar secretul de la stripe listen în dezvoltare este din nou diferit. O eroare foarte comună „verificarea semnăturii eșuată” este pur și simplu folosirea uneia greșite.
Pasul 2. Verificați semnătura de pe corpul brut
Stripe semnează fiecare cerere. Antetul Stripe-Signature conține un marcaj de timp și una sau mai multe semnături calculate ca HMAC-SHA256 de timestamp.payload cu secretul punctului final. Biblioteca verifică semnătura și respinge evenimentele mai vechi de cinci minute în mod implicit.
Detaliul important: verificarea necesită octeții bruti exacti trimiși Stripe. Dacă un cadru analizează JSON și îl re-codificați, spațiul alb se schimbă și semnătura nu mai corespunde.
<?php
// public/webhooks/stripe.php (plain PHP)
require __DIR__ . '/../../vendor/autoload.php';
$payload = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$secret = getenv('STRIPE_WEBHOOK_SECRET');
try {
$event = \Stripe\Webhook::constructEvent($payload, $sigHeader, $secret);
} catch (\UnexpectedValueException $e) {
http_response_code(400); // invalid JSON
exit;
} catch (\Stripe\Exception\SignatureVerificationException $e) {
http_response_code(400); // bad or missing signature
exit;
}
În Laravel, citiți $request->getContent() în loc de $request->all() și excludeți ruta de la protecția CSRF (webhook-urile nu poartă un token CSRF; semnătura este protecția). Puneți ruta în afara grupului de middleware de sesiune web, dacă puteți.
Pasul 3. Faceți handler-ul idempotent
Idempotent înseamnă: procesarea aceluiași eveniment de două ori dă același rezultat ca și procesarea lui o dată. Cea mai simplă abordare fiabilă este un tabel cu ID-uri de eveniment procesate cu un index unic. Introduceți mai întâi; dacă inserarea eșuează deoarece ID-ul există, ați văzut deja evenimentul.
CREATE TABLE stripe_events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
event_id VARCHAR(64) NOT NULL,
type VARCHAR(100) NOT NULL,
payload JSON NOT NULL,
status ENUM('received','processed','failed') NOT NULL DEFAULT 'received',
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP NULL,
UNIQUE KEY uniq_event (event_id)
);
try {
$pdo->prepare('INSERT INTO stripe_events (event_id, type, payload) VALUES (?, ?, ?)')
->execute([$event->id, $event->type, $payload]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') { // duplicate key: already received
http_response_code(200);
exit;
}
throw $e;
}
Stocarea încărcăturii brute vă oferă o pistă de audit: când un client întreabă „de ce am fost taxat de două ori?”, puteți reda exact ceea ce v-a spus Stripe. De asemenea, face posibilă reprocesarea evenimentelor eșuate după ce remediați o eroare.
Pasul 4. Confirmați rapid, procesați separat
Stripe așteaptă un răspuns în câteva secunde. Dacă managerul dvs. trimite e-mailuri, apelează alte API-uri sau generează PDF-uri în linie, acesta va expira, Stripe va reîncerca și veți face din nou aceeași muncă. Faceți minimul din cerere - verificați, stocați, răspundeți 200 - și rulați logica reală într-un lucrător de coadă (cozi Laravel, Symfony Messenger, status = 'received' sau un lucrător gestionat de supervizor).
Dacă evenimentul nu poate fi stocat deloc (baza de date oprită), returnați o stare 5xx. Acesta este singurul caz în care doriți ca Stripe să reîncerce mai târziu.
Pasul 5. Gestionați evenimentele care contează
Nu aveți nevoie de orice tip de eveniment. Abonați-vă numai la ceea ce folosește logica dvs. de afaceri; mai puține abonamente înseamnă mai puțin zgomot și o suprafață de atac mai mică. Un set tipic:
checkout.session.completed- clientul a terminat Checkout. Căutați comanda folosindclient_reference_idsau metadatele pe care le-ați setat la crearea sesiunii.payment_intent.succeededșipayment_intent.payment_failed- pentru fluxuri de plată personalizate.invoice.paidșiinvoice.payment_failed- facturarea abonamentului.customer.subscription.updatedșicustomer.subscription.deleted- modificări și anulări ale planului.charge.refundedșicharge.dispute.created- banii se retrag.
switch ($event->type) {
case 'checkout.session.completed':
$session = $event->data->object;
$orderId = $session->client_reference_id;
if ($session->payment_status === 'paid') {
markOrderPaid($orderId, $session->payment_intent);
}
break;
case 'invoice.payment_failed':
notifyBillingProblem($event->data->object->customer);
break;
case 'charge.refunded':
markOrderRefunded($event->data->object->payment_intent);
break;
default:
// unknown or unsubscribed type: acknowledge and ignore
break;
}
Două obiceiuri previn majoritatea bug-urilor. În primul rând, nu aveți încredere niciodată în sumele din browser: comparați suma plătită și moneda în eveniment cu comanda din baza de date. În al doilea rând, pentru că evenimentele pot fi neregulate, faceți schimbările de stare monotone: nu mutați o comandă de la „plătit” înapoi la „în așteptare”, deoarece un eveniment mai vechi a sosit cu întârziere. Când aveți îndoieli, preluați obiectul curent din API în loc să vă bazați pe instantaneul evenimentului.
Pasul 6. Testați local cu Stripe CLI
Stripe CLI redirecționează evenimentele reale în modul de testare către mașina dvs., astfel încât să nu aveți nevoie de un tunel public:
stripe login
stripe listen --forward-to http://localhost:8000/webhooks/stripe
# in another terminal:
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed
stripe listen afișează un secret temporar whsec_. Puneți asta în .env local. De asemenea, testați în mod intenționat căile nefericite: trimiteți o cerere cu o semnătură greșită (așteptați-vă la 400), trimiteți același eveniment de două ori (așteptați-vă la un efect de bază de date) și opriți-vă lucrătorul la coadă pentru un minut pentru a vedea că evenimentele se adună în loc să dispară.
Greșeli frecvente pe care le vedem în audituri
- Folosind redirecționarea ca dovadă a plății. Pagina
success_urlpoate fi deschisă de oricine; afișați „confirmarea plății” până la sosirea webhook-ului. - Se analizează JSON înainte de verificare. Corpurile re-codate rup semnătura; analizatorii de corp cadru sunt un vinovat frecvent.
- Un secret pentru testare și live. Modul de testare și modul live au chei separate și puncte finale webhook separate.
- Fără înregistrare. Fără evenimente stocate și un canal de eșec vizibil, erorile de plată sunt găsite de clienți.
- Manipulatori lenți. Orice lucru peste câteva secunde declanșează reîncercări și lucru duplicat.
- Blocarea Stripe la firewall sau WAF. Dacă utilizați Cloudflare sau alt WAF, permiteți calea webhook și verificați dacă regulile bot nu returnează 403 la serverele lui Stripe.
Monitorizare: cum știi că continuă să funcționeze
Un webhook se poate rupe luni după lansare: o modificare implementată redenumește o coloană, un secret este rotit, un certificat expiră. Tabloul de bord Stripe afișează încercările de livrare și eșecurile pentru fiecare punct final, iar Stripe vă trimite e-mail atunci când un punct final continuă să eșueze. De partea dvs., atenționați cu privire la trei lucruri: un număr tot mai mare de evenimente received sau failed, o scădere bruscă a evenimentelor pe oră și orice rată 4xx sau 5xx pe ruta webhook. O mică lucrare zilnică de reconciliere care compară comenzile plătite din baza ta de date cu lista Stripe de plăți reușite prinde restul.
Cum putem ajuta
Proiectăm și construim plăți de producție și integrări webhook cu verificări ale semnăturii, idempotency, reîncercări și jurnalele pe care echipa ta le poate citi. Dacă aveți deja un flux Stripe și doriți o a doua pereche de ochi, o revizuire a arhitecturii sau o consultare este cea mai rapidă modalitate de a găsi punctele slabe. Pentru o imagine mai amplă a mutarii evenimentelor critice din Zapier sau Make, citiți când nu se întrerupe codul și pentru un alt furnizor, consultați ghidul nostru pentru integrarea WayForPay în PHP.
Întrebări frecvente
De ce nu eșuează verificarea semnăturii Stripe webhook?
Cauzele obișnuite sunt folosirea unui secret de semnare greșit (test vs live, CLI vs endpoint tablou de bord), verificarea unui corp analizat sau recodificat în locul corpului cererii brute sau un proxy care modifică sarcina utilă. Verificați cu octeții bruti exacti cu propriul secret whsec_ al punctului final.
Cum pot preveni procesarea unui eveniment Stripe de două ori?
Stocați fiecare ID de eveniment într-un tabel cu un index unic și introduceți-l înainte de a lucra. Dacă inserarea eșuează cu o cheie duplicată, returnați 200 și omiteți procesarea. De asemenea, faceți ca acțiunea de afaceri în sine să fie repetată în siguranță.
Ce cod de răspuns ar trebui să returneze un webhook Stripe ?
Returnați un cod 2xx (200 sau 204) imediat ce evenimentul este verificat și stocat. Returnați 400 pentru o semnătură nevalidă și 5xx numai atunci când nu puteți stoca evenimentul și doriți ca Stripe să încerce din nou.
Cât timp reîncercă Stripe webhook-urile eșuate?
În modul live, Stripe reîncercă cu retragere exponențială timp de până la trei zile. În modul de testare, fereastra de reîncercare este mult mai scurtă, așa că nu vă bazați pe reîncercări în modul de testare pentru a judeca comportamentul producției.
Pot testa webhook-urile Stripe fără o adresă URL publică?
Da. Rulați stripe listen --forward-to cu Stripe CLI pentru a redirecționa evenimentele de testare către localhost și utilizați stripe trigger pentru a genera evenimente eșantion.
Apel de descoperire gratuit
Adu un stack haotic sau o integrare blocată - Integrono mapează cel mai inteligent prim pas.
Primești o evaluare senior pe capcane de buget, dependențe și dacă merită să cumperi, să lipești sau să construiești - înainte să înceapă munca plătită. CEO-ul rămâne în buclă.