Stripe webhooki w PHP: niezawodny moduł obsługi, któremu możesz zaufać
Webhook Stripe, który działa na Twoim laptopie, nie jest jeszcze webhookiem, któremu możesz zaufać za pieniądze. Ten przewodnik pokazuje, jak weryfikować podpisy, szybko potwierdzać, deduplikować zdarzenia i obsługiwać ponowne próby w zwykłym PHP i Laravel, z kodem, który można wkleić do projektu.
W skrócie
- Traktuj webhooka, a nie przekierowanie przeglądarki, jako źródło prawdy o statusie płatności.
- Przeczytaj nieprzetworzoną treść żądania i sprawdź nagłówek
Stripe-Signatureprzed dotknięciem ładunku. - Szybko zwróć odpowiedź 2xx i wykonaj prawdziwą pracę w kolejce lub po zapisaniu zdarzenia.
- Przechowuj identyfikator zdarzenia z unikalnym indeksem: Stripe może dostarczyć to samo wydarzenie więcej niż raz i nie w kolejności.
- Przetestuj za pomocą Stripe CLI (
stripe listenistripe trigger) przed rozpoczęciem transmisji na żywo.
Jeśli akceptujesz płatności za pomocą Stripe, webhooki Stripe w PHP decydują o tym, czy zamówienia zostaną oznaczone jako opłacone, aktywowane zostaną subskrypcje, a zwroty środków zostaną odzwierciedlone w Twojej bazie danych. Strona kasy to tylko drzwi wejściowe: klienci zamykają karty, banki potwierdzają spóźnienie, a niektóre metody płatności rozliczają się kilka minut lub dni później. Webhook to wiadomość od Stripe, która mówi, co naprawdę się wydarzyło.
Budujemy i utrzymujemy integracje płatności dla klientów i dla naszych własnych produktów (na przykład Bill, który przyjmuje płatności Stripe i PayPal na publicznej stronie płatności). Ten artykuł to lista kontrolna, którą stosujemy, pisząc punkt końcowy webhooka Stripe w PHP.
Jak działają webhooki Stripe w ciągu jednej minuty
Rejestrujesz adres URL HTTPS w Panelu Stripe (lub poprzez API) i wybierasz, jakie typy zdarzeń chcesz otrzymywać. Gdy coś się stanie – zakończy się sesja realizacji transakcji, faktura zostanie zapłacona, opłata zostanie zwrócona – Stripe wysyła żądanie POST z obiektem zdarzenia JSON na ten adres URL. Twój serwer musi odpowiedzieć kodem stanu 2xx. Jeśli tak się nie stanie, Stripe ponawia próbę z wykładniczym wycofywaniem przez maksymalnie trzy dni w trybie na żywo.
Trzy fakty kształtują cały projekt:
- Wydarzenia mogą nadejść więcej niż raz. Ponowne próby i problemy z siecią sprawiają, że duplikaty są normalne.
- Wydarzenia mogą pojawić się poza kolejnością. Możesz zobaczyć
invoice.paidprzedcustomer.subscription.created. - Każdy może publikować posty pod Twoim adresem URL. Bez weryfikacji podpisu osoba atakująca może oznaczyć własne zamówienie jako opłacone.
Krok 1. Zainstaluj pakiet SDK i utwórz punkt końcowy
Użyj oficjalnej biblioteki stripe/stripe-php i utrzymuj punkt końcowy w tajemnicy (zaczyna się od whsec_) w zmiennej środowiskowej, nigdy w repozytorium.
composer require stripe/stripe-php
# .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Każdy punkt końcowy elementu webhook w panelu kontrolnym ma swój własny sekret podpisywania, a sekret stripe listen w fazie rozwoju znów jest inny. Bardzo częstym błędem „weryfikacja podpisu nie powiodła się” jest po prostu użycie niewłaściwego podpisu.
Krok 2. Zweryfikuj podpis na surowym korpusie
Stripe podpisuje każdą prośbę. Nagłówek Stripe-Signature zawiera sygnaturę czasową i jeden lub więcej podpisów obliczonych jako HMAC-SHA256 timestamp.payload z sekretem punktu końcowego. Biblioteka domyślnie sprawdza sygnaturę i odrzuca zdarzenia starsze niż pięć minut.
Ważny szczegół: weryfikacja wymaga dokładnie surowych bajtów wysłanych przez Stripe. Jeśli struktura analizuje JSON i ponownie go kodujesz, białe znaki zmieniają się i podpis nie jest już zgodny.
<?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;
}
W Laravel przeczytaj $request->getContent() zamiast $request->all() i wyklucz trasę z ochrony CSRF (webhooki nie przenoszą tokena CSRF; zabezpieczeniem jest podpis). Jeśli możesz, umieść trasę poza grupą oprogramowania pośredniczącego sesji web.
Krok 3. Ustaw procedurę obsługi jako idempotentną
Idempotent oznacza: dwukrotne przetworzenie tego samego zdarzenia daje taki sam wynik, jak przetworzenie go jednorazowo. Najprostszym niezawodnym podejściem jest tabela przetworzonych identyfikatorów zdarzeń z unikalnym indeksem. Wstaw najpierw; jeśli wstawienie nie powiedzie się, ponieważ identyfikator istnieje, zdarzenie już widziałeś.
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;
}
Przechowywanie surowego ładunku zapewnia ścieżkę audytu: gdy klient pyta „dlaczego pobrano ode mnie podwójną opłatę?”, możesz odtworzyć dokładnie to, co powiedział Ci Stripe. Umożliwia także ponowne przetworzenie nieudanych zdarzeń po naprawieniu błędu.
Krok 4. Potwierdź szybko, przetwarzaj oddzielnie
Stripe oczekuje odpowiedzi w ciągu kilku sekund. Jeśli Twój moduł obsługi wysyła e-maile, wywołuje inne interfejsy API lub generuje pliki PDF w trybie online, upłynie limit czasu, Stripe spróbuje ponownie, a Ty ponownie wykonasz tę samą pracę. Wykonaj minimum w żądaniu - zweryfikuj, zapisz, odpowiedz 200 - i uruchom prawdziwą logikę w procesie roboczym kolejki (kolejki Laravel, Symfony Messenger, status = 'received' lub proces roboczy zarządzany przez przełożonego).
Jeśli w ogóle nie można zapisać zdarzenia (baza danych nie działa), zwróć status 5xx. To jedyny przypadek, w którym chcesz, aby Stripe ponowił próbę później.
Krok 5. Zajmij się ważnymi wydarzeniami
Nie potrzebujesz każdego typu zdarzenia. Subskrybuj tylko to, z czego korzysta Twoja logika biznesowa; mniej subskrypcji oznacza mniej hałasu i mniejszą powierzchnię ataku. Typowy zestaw:
checkout.session.completed- klient zakończył realizację transakcji. Sprawdź swoje zamówienie, korzystając zclient_reference_idlub metadanych ustawionych podczas tworzenia sesji.payment_intent.succeededipayment_intent.payment_failed- dla niestandardowych przepływów płatności.invoice.paidiinvoice.payment_failed- rozliczenie subskrypcji.customer.subscription.updatedicustomer.subscription.deleted- zmiany i anulowanie planów.charge.refundedicharge.dispute.created– pieniądze zostaną zwrócone.
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;
}
Dwa nawyki zapobiegają większości błędów. Po pierwsze, nigdy nie ufaj kwotom z przeglądarki: porównaj zapłaconą kwotę i walutę w wydarzeniu z zamówieniem w swojej bazie danych. Po drugie, ponieważ zdarzenia mogą być nie w porządku, spraw, aby zmiany stanu były monotoniczne: nie przesuwaj zamówienia z „opłaconego” z powrotem do „oczekującego”, ponieważ starsze wydarzenie przybyło z opóźnieniem. W razie wątpliwości pobierz bieżący obiekt z interfejsu API, zamiast polegać na migawce zdarzenia.
Krok 6. Przetestuj lokalnie za pomocą Stripe CLI
Stripe CLI przekazuje rzeczywiste zdarzenia w trybie testowym do Twojej maszyny, więc nie potrzebujesz publicznego tunelu:
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 wypisuje tymczasowy sekret whsec_. Umieść to w swoim lokalnym pliku .env. Celowo przetestuj także nieszczęśliwe ścieżki: wyślij żądanie z błędnym podpisem (spodziewaj się 400), wyślij dwukrotnie to samo zdarzenie (spodziewaj się jednego efektu bazy danych) i zatrzymaj na minutę pracownika kolejki, aby zobaczyć, że zdarzenia nawarstwiają się, a nie znikają.
Typowe błędy, które widzimy podczas audytów
- Użycie przekierowania jako dowodu płatności. Stronę
success_urlmoże otworzyć każdy; wyświetlaj „potwierdzenie płatności” do momentu pojawienia się webhooka. - Analizowanie JSON przed weryfikacją. Ponownie zakodowane ciała łamią podpis; Częstym winowajcą są analizatory treści frameworka.
- Jeden sekret do przetestowania i na żywo. Tryb testowy i tryb na żywo mają osobne klucze i oddzielne punkty końcowe elementu webhook.
- Brak logowania. Bez zapisanych zdarzeń i widocznego kanału awarii klienci wykrywają błędy w płatnościach.
- Powolni operatorzy. Wszystko, co trwa dłużej niż kilka sekund, powoduje ponowne próby i zduplikowanie pracy.
- Blokowanie Stripe na zaporze lub WAF. Jeśli używasz Cloudflare lub innego WAF, zezwól na ścieżkę webhooka i sprawdź, czy reguły bota nie zwracają 403 na serwery Stripe.
Monitorowanie: skąd wiesz, że działa
Element webhook może zepsuć się kilka miesięcy po uruchomieniu: wdrożona zmiana zmienia nazwę kolumny, klucz tajny ulega rotacji, a certyfikat wygasa. Panel Stripe pokazuje próby i niepowodzenia dostarczania dla poszczególnych punktów końcowych, a Stripe wysyła Ci e-maile, gdy punkt końcowy przestaje działać. Ze swojej strony zwracaj uwagę na trzy rzeczy: rosnącą liczbę zdarzeń received lub failed, nagły spadek liczby zdarzeń na godzinę oraz dowolną stawkę 4xx lub 5xx na trasie webhooka. Resztę wychwytuje małe, codzienne zadanie uzgadniania, które porównuje opłacone zamówienia w Twojej bazie danych z listą udanych płatności Stripe.
Jak możemy pomóc
Projektujemy i budujemy integracje płatności produkcyjnych i webhooków z kontrolą podpisów, idempotencją, ponownymi próbami i dziennikami, które Twój zespół może odczytać. Jeśli masz już Stripe i potrzebujesz drugiej pary oczu, przegląd architektury lub konsultacja to najszybszy sposób na znalezienie słabych punktów. Aby uzyskać szerszy obraz przenoszenia krytycznych zdarzeń z Zapier lub Make, przeczytaj, kiedy nie ma awarii kodu, a w przypadku innego dostawcy zapoznaj się z naszym przewodnikiem po integracji WayForPay z PHP.
Najczęściej zadawane pytania
Dlaczego weryfikacja podpisu webhooka Stripe kończy się niepowodzeniem?
Typowymi przyczynami są użycie nieprawidłowego sekretu podpisywania (test vs działanie na żywo, CLI vs punkt końcowy pulpitu nawigacyjnego), weryfikacja przeanalizowanej lub ponownie zakodowanej treści zamiast nieprzetworzonej treści żądania lub serwer proxy modyfikujący ładunek. Sprawdź dokładnie surowe bajty za pomocą własnego sekretu whsec_ punktu końcowego.
Jak zapobiec dwukrotnemu przetwarzaniu zdarzenia Stripe ?
Zapisz każdy identyfikator zdarzenia w tabeli z unikalnym indeksem i wstaw go przed wykonaniem jakiejkolwiek pracy. Jeśli wstawienie nie powiedzie się z powodu zduplikowanego klucza, zwróć 200 i pomiń przetwarzanie. Spraw, aby samo działanie biznesowe było bezpieczne i można je było powtórzyć.
Jaki kod odpowiedzi powinien zwrócić webhook Stripe ?
Zwróć kod 2xx (200 lub 204), gdy tylko zdarzenie zostanie zweryfikowane i zapisane. Zwróć 400 w przypadku nieprawidłowego podpisu i 5xx tylko wtedy, gdy nie możesz zapisać zdarzenia i chcesz, aby Stripe ponowił próbę.
Jak długo Stripe ponawia nieudane próby webhooków?
W trybie na żywo Stripe ponawia próby z wykładniczym wycofywaniem przez maksymalnie trzy dni. W trybie testowym okno ponownych prób jest znacznie krótsze, dlatego nie należy polegać na ponownych próbach w trybie testowym, aby ocenić zachowanie produkcyjne.
Czy mogę testować webhooki Stripe bez publicznego adresu URL?
Tak. Uruchom stripe listening --forward-to za pomocą Stripe CLI, aby przekazać zdarzenia testowe do hosta lokalnego i użyć wyzwalacza stripe do wygenerowania przykładowych zdarzeń.
Bezpłatna rozmowa odkrywcza
Bałagan w stacku albo zawieszona integracja - Integrono zaplanuje najmądrzejszy pierwszy ruch.
Dostajesz senior-ocenę pułapek budżetowych, zależności i tego, czy kupować, sklejać czy budować - zanim zacznie się płatna praca. CEO zostaje w pętli.