Блог

Интеграции 9 мин. четене

Stripe webhooks в PHP: надежден манипулатор, на който можете да се доверите

Уеб кукичката Stripe, която работи на вашия лаптоп, все още не е уеб кукичка, на която можете да се доверите с пари. Това ръководство показва как да проверявате подписи, да потвърждавате бързо, да дедупликирате събития и да обработвате повторни опити в обикновен PHP и Laravel, с код, който можете да поставите в проект.

Корица на статията: Stripe webhooks в PHP: надежден манипулатор, на който можете да се доверите

Накратко

  • Отнасяйте се към уебкукичката, а не към пренасочването на браузъра, като към източник на истина за състоянието на плащането.
  • Прочетете необработеното тяло на заявката и проверете заглавката Stripe-Signature преди да докоснете полезния товар.
  • Върнете бързо отговор 2xx и свършете истинската работа в опашка или след съхраняване на събитието.
  • Съхранявайте идентификатора на събитието с уникален индекс: Stripe може да достави едно и също събитие повече от веднъж и неправилно.
  • Тествайте със Stripe CLI (stripe listen и stripe trigger), преди да стартирате на живо.

Ако приемате плащания със Stripe, уеб куките Stripe в PHP са частта, която решава дали поръчките се маркират като платени, абонаментите се активират и възстановяванията се отразяват във вашата база данни. Страницата за плащане е само входната врата: клиентите затварят раздели, банките потвърждават със закъснение и някои методи на плащане се уреждат след минути или дни. Webhook е съобщението от Stripe, което казва какво наистина се е случило.

Ние изграждаме и поддържаме интеграции за плащане за клиенти и за нашите собствени продукти (например Bill, който приема плащания Stripe и PayPal на публична страница за плащане). Тази статия е контролният списък, който следваме, когато пишем крайна точка на Stripe webhook в PHP.

Как Stripe webhooks работят за една минута

Регистрирате HTTPS URL в таблото за управление Stripe (или чрез API) и избирате кои типове събития да получавате. Когато нещо се случи – сесията за плащане завърши, фактурата е платена, таксата е възстановена – Stripe изпраща POST заявка с JSON обект на събитие към този URL адрес. Вашият сървър трябва да отговори с код за състояние 2xx. Ако не стане, Stripe прави повторен опит с експоненциално забавяне за до три дни в режим на живо.

Три факта оформят целия дизайн:

  • Събитията могат да пристигнат повече от веднъж. Повторните опити и мрежовите проблеми правят дубликатите нормални.
  • Събитията могат да пристигнат не по ред. Може да видите invoice.paid преди customer.subscription.created.
  • Всеки може да ПУБЛИКУВА на вашия URL. Без проверка на подписа нападателят може да маркира собствената си поръчка като платена.

Стъпка 1. Инсталирайте SDK и създайте крайната точка

Използвайте официалната библиотека, stripe/stripe-php, и пазете крайната точка в тайна (започва с whsec_) в променлива на средата, никога в хранилището.

composer require stripe/stripe-php

# .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

Всяка крайна точка на уеб кукичка в таблото за управление има своя собствена тайна за подписване, а тайната от stripe listen в разработка отново е различна. Много често срещан бъг „неуспешна проверка на подписа“ е просто използването на грешен.

Стъпка 2. Проверете подписа върху необработеното тяло

Stripe подписва всяка заявка. Заглавката Stripe-Signature съдържа клеймо за време и един или повече подписи, изчислени като HMAC-SHA256 на timestamp.payload с вашата тайна на крайната точка. Библиотеката проверява подписа и по подразбиране отхвърля събития, по-стари от пет минути.

Важната подробност: проверката се нуждае от точните необработени байтове, изпратени от Stripe. Ако рамка анализира JSON и вие го кодирате отново, интервалът се променя и подписът вече не съвпада.

<?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;
}

В Laravel прочетете $request->getContent() вместо $request->all() и изключете маршрута от CSRF защита (webhooks не носят CSRF токен; подписът е защитата). Поставете маршрута извън групата на мидълуера на web сесията, ако можете.

Стъпка 3. Направете манипулатора идемпотентен

Идемпотентно означава: обработката на едно и също събитие два пъти дава същия резултат като обработката му веднъж. Най-простият надежден подход е таблица с обработени идентификатори на събития с уникален индекс. Поставете първо; ако вмъкването е неуспешно, защото идентификаторът съществува, вече сте видели събитието.

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;
}

Съхраняването на необработения полезен товар ви дава одитна пътека: когато клиент попита „защо бях таксуван два пъти?“, можете да повторите точно това, което Stripe ви е казал. Също така прави възможно повторното обработване на неуспешни събития, след като коригирате грешка.

Стъпка 4. Потвърдете бързо, обработете отделно

Stripe очаква отговор в рамките на няколко секунди. Ако вашият манипулатор изпраща имейли, извиква други API или генерира PDF файлове на линия, ще изтече времето за изчакване, Stripe ще опита отново и вие ще свършите същата работа отново. Направете минимума в заявката – проверете, съхранете, отговорете 200 – и изпълнете истинската логика в queuer worker ( Laravel queues, Symfony Messenger, status = 'received' или worker, управляван от супервайзор).

Ако събитието изобщо не може да бъде съхранено (базата данни е изключена), върнете статус 5xx. Това е единственият случай, в който искате Stripe да опита отново по-късно.

Стъпка 5. Справете се със събитията, които имат значение

Не се нуждаете от всеки тип събитие. Абонирайте се само за това, което използва вашата бизнес логика; по-малкото абонаменти означава по-малко шум и по-малка повърхност за атака. Типичен комплект:

  • checkout.session.completed - клиентът завърши Checkout. Потърсете поръчката си, като използвате client_reference_id или метаданни, които сте задали при създаването на сесията.
  • payment_intent.succeeded и payment_intent.payment_failed - за персонализирани потоци на плащане.
  • invoice.paid и invoice.payment_failed - фактуриране на абонамент.
  • customer.subscription.updated и customer.subscription.deleted - промени и анулации на планове.
  • charge.refunded и charge.dispute.created - парите се връщат обратно.
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;
}

Два навика предотвратяват повечето грешки. Първо, никога не се доверявайте на суми от браузъра: сравнете платената сума и валута в събитието с поръчката във вашата база данни. Второ, тъй като събитията може да не са в ред, направете промените в състоянието монотонни: не премествайте поръчка от „платена“ обратно към „чакаща“, защото по-старо събитие е пристигнало със закъснение. Когато се съмнявате, извлечете текущия обект от API, вместо да разчитате на моментната снимка на събитието.

Стъпка 6. Тествайте локално с Stripe CLI

Stripe CLI препраща реални събития в тестов режим към вашата машина, така че нямате нужда от публичен тунел:

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 отпечатва временна тайна whsec_. Поставете това във вашия локален .env. Също така нарочно тествайте неудовлетворените пътища: изпратете заявка с грешен подпис (очаквайте 400), изпратете едно и също събитие два пъти (очаквайте един ефект от базата данни) и спрете вашия работник на опашка за минута, за да видите, че събитията се натрупват, вместо да изчезнат.

Често срещани грешки, които виждаме при одитите

  • Използване на пренасочването като доказателство за плащане. Страницата success_url може да бъде отворена от всеки; покажете „потвърждаване на вашето плащане“, докато пристигне уебкукичката.
  • Анализ на JSON преди проверка. Прекодираните тела нарушават подписа; анализаторите на тялото на рамката са чест виновник.
  • Една тайна за тест и на живо. Тестът и режимът на живо имат отделни ключове и отделни крайни точки на уеб кукичка.
  • Без регистриране. Без съхранени събития и видим канал за неизправност, грешките в плащанията се откриват от клиентите.
  • Бавни манипулатори. Всичко над няколко секунди задейства повторни опити и дублиране на работата.
  • Блокиране на Stripe на защитната стена или WAF. Ако използвате Cloudflare или друг WAF, разрешете пътя на webhook и проверете дали правилата на бота не връщат 403 към сървърите на Stripe.

Мониторинг: как знаете, че продължава да работи

Уеб кукичката може да се повреди месеци след стартирането: въведена промяна преименува колона, тайна се завърта, сертификат изтича. Таблото за управление Stripe показва опити и неуспешни доставки за всяка крайна точка, а Stripe ви изпраща имейл, когато дадена крайна точка продължава да се проваля. От ваша страна предупреждавайте за три неща: нарастващ брой събития в received или failed, внезапен спад на събития на час и всяка скорост 4xx или 5xx по маршрута на webhook. Малка ежедневна работа за съгласуване, която сравнява платените поръчки във вашата база данни със списъка на Stripe с успешни плащания, улавя останалото.

Как можем да помогнем

Ние проектираме и изграждаме производствени плащания и интеграции на webhook с проверки на подписи, идемпотентност, повторни опити и регистрационни файлове, които вашият екип може да прочете. Ако вече имате Stripe flow и искате втори чифт очи, прегледът на архитектурата или консултацията е най-бързият начин да намерите слабите места. За по-широка картина на преместване на критични събития извън Zapier или Make, прочетете кога кодът се поврежда, а за друг доставчик вижте нашето ръководство за интегриране WayForPay в PHP.

Често задавани въпроси

Защо проверката на подписа на уебкукичката Stripe е неуспешна?

Обичайните причини са използването на грешна тайна за подписване (тест срещу живо, CLI срещу крайна точка на таблото), проверка на анализирано или повторно кодирано тяло вместо необработено тяло на заявка или прокси, който променя полезния товар. Проверете спрямо точните необработени байтове със собствената тайна whsec_ на крайната точка.

Как да предотвратя обработката на Stripe събитие два пъти?

Съхранявайте всеки идентификатор на събитие в таблица с уникален индекс и го вмъквайте, преди да извършите каквато и да е работа. Ако вмъкването е неуспешно с дублиран ключ, върнете 200 и пропуснете обработката. Също така направете самото бизнес действие безопасно за повторение.

Какъв код на отговор трябва да върне Stripe webhook?

Върнете код 2xx (200 или 204) веднага щом събитието бъде потвърдено и съхранено. Върнете 400 за невалиден подпис и 5xx само когато не можете да съхраните събитието и искате Stripe да опита отново.

Колко време Stripe прави повторен опит при неуспешни уеб кукички?

В режим на живо Stripe прави повторни опити с експоненциално забавяне за до три дни. В тестов режим прозорецът за повторни опити е много по-кратък, така че не разчитайте на повторни опити в тестов режим, за да прецените производственото поведение.

Мога ли да тествам уеб кукички Stripe без публичен URL?

да Изпълнете stripe listen --forward-to с Stripe CLI, за да препратите тестови събития към localhost и използвайте stripe trigger, за да генерирате примерни събития.

Безплатно обаждане за откриване

Донесете разхвърлян стек или блокирала интеграция - Integrono ще набележи най-умния първи ход.

Получавате експертна оценка на бюджетни капани, зависимости и дали да купите, слепите или изградите - преди да започне платената работа. CEO остава в контура.