Czym są podpisane webhooki Payora
Podpisane webhooki Payora to żądania HTTP zawierające podpis cyfrowy, dzięki któremu endpoint PHP może sprawdzić, czy zdarzenie rzeczywiście pochodzi od Payora i nie zostało zmienione po drodze. Jeśli chcesz wiedzieć, jak zweryfikować podpis webhooka Payora w PHP, podstawowy przebieg jest prosty: Payora wysyła zdarzenie, serwer odczytuje surowe dane żądania, a kod weryfikuje podpis, zanim zaufa czemukolwiek w treści.
Ta kontrola jest ważna, ponieważ webhook może nadejść od dowolnego klienta, który zna Twój adres URL. Osoba o złych zamiarach może skopiować endpoint z logów, a następnie wysłać żądaniem POST fałszywe zdarzenie płatnicze z poprawnie wyglądającym obiektem JSON. Podpis udaremnia taki atak.
Samo zdarzenie jest przydatne dopiero po weryfikacji. Wcześniej to tylko tekst.
Można to porównać do ponumerowanego biletu przy wejściu. Treść to wiadomość, podpis to pieczęć, a kod PHP rozstrzyga, czy obie części do siebie pasują. Jeśli tak, przetwarzasz zdarzenie. Jeśli nie, od razu je odrzucasz.
Jeśli obsługujesz już płatności na swojej stronie, podobne zasady obowiązują też w innych elementach systemu. Z poradnika o bramce płatności kryptowalutowych dla e-commerce dowiesz się, że warto akceptować mniej, dokładniej weryfikować i dopiero potem aktualizować zamówienie.
Wymagania wstępne dla endpointu webhooka w PHP
Nie potrzebujesz do tego rozbudowanego frameworka. Wystarczy endpoint działający na PHP 8.x; PHP 7.4 może sprawdzić się w niektórych projektach. Kod musi mieć dostęp do surowej treści żądania i nagłówków wysyłanych przez Payora. Oznacza to, że konfiguracja serwera powinna umożliwiać standardowy dostęp do żądań HTTP, bez niestandardowej warstwy, która przepisuje lub usuwa nagłówki.
Korzystaj z HTTPS. Bez TLS webhook jest przesyłany jawnym tekstem, a sam podpis nie rozwiązuje problemu słabego zabezpieczenia połączenia. Podstawą są certyfikat, prawidłowa konfiguracja wirtualnego hosta i publiczny, dostępny adres URL.
W praktyce znaczenie mają dwa rozszerzenia PHP: hash do obliczeń HMAC oraz — jeśli obsługujesz JSON — wbudowane funkcje JSON dostępne w PHP. Do testów możesz też użyć cURL, ale sam endpoint webhooka go nie wymaga.
Potrzebujesz również sekretu Payora lub klucza podpisującego powiązanego z konfiguracją webhooka na koncie Payora. Przechowuj tę wartość poza katalogiem głównym serwera WWW. Nie umieszczaj jej w repozytorium Git. Upubliczniony sekret sprawia, że weryfikacja staje się fikcją.
Jeszcze jedna kwestia: endpoint powinien akceptować wyłącznie żądania POST. Na żądania GET może odpowiadać kodem 405. To drobne ograniczenie ułatwia diagnozowanie problemów, bo zawęża ruch, który trzeba analizować.
Odczytaj surowe dane webhooka i nagłówki
Weryfikacja podpisu zależy od dokładnych bajtów wysłanych przez Payora. W PHP odczytaj surową treść żądania za pomocą file_get_contents('php://input'). Nie odtwarzaj jej na podstawie $_POST ani zdekodowanego JSON-a, bo każdy z tych kroków może zmienić odstępy, kolejność kluczy lub kodowanie.
Ta sama zasada dotyczy nagłówków. Potrzebujesz nagłówka z podpisem. Jeśli jest dostępna, użyj funkcji getallheaders(); jeśli środowisko nie udostępnia tej funkcji, odczytaj zmienne serwera, takie jak $_SERVER['HTTP_X_...']. Drobne różnice między środowiskami hostingowymi mogą mieć tu znaczenie.
Wyobraź sobie treść webhooka o długości 412 bajtów. Po zdekodowaniu i ponownym zakodowaniu możesz nadal widzieć te same pola, ale bajty nie będą już identyczne. Taka pojedyncza różnica wystarczy, by weryfikacja się nie powiodła.
Zapamiętaj tę kolejność: najpierw przechwyć dane, potem je analizuj. Najpierw zapisz surowy ciąg znaków. Następnie sprawdź nagłówki. Dopiero potem zajmij się strukturą treści.
Ta sama zasada oszczędza czas podczas testowania płatności przed uruchomieniem serwisu. W artykule o testowaniu płatności kryptowalutowej podobne podejście zastosowano do zdarzeń w środowisku testowym — użyteczność testu zależy od tego, czy dane pozostają niezmienione.
Odtwórz oczekiwany podpis w PHP
Najczęściej stosuje się HMAC obliczany na podstawie surowej treści i wspólnego sekretu. Zacznij od algorytmu wskazanego w dokumentacji webhooków Payora. Jeśli dokumentacja podaje SHA-256, użyj SHA-256. Jeśli określa prefiks, tag wersji lub format znacznika czasu, uwzględnij dokładnie tę strukturę. Tę metodę opisuje też ogólne podejście do weryfikacja HMAC webhooka w PHP: podpis musi być obliczony z dokładnie tych danych i w takim formacie, jakich wymaga dostawca.
Typowy schemat wygląda tak: utwórz podpisywaną wiadomość ze znacznika czasu i surowej treści, a następnie użyj funkcji hash_hmac() ze wspólnym sekretem i określonym algorytmem skrótu. W zależności od specyfikacji wynik może wymagać kodowania szesnastkowego lub base64. Nie zgaduj formatu — sprawdź go w ustawieniach webhooka.
Oto minimalny przykład w PHP, z miejscami, w których format Payora może się różnić:
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_PAYORA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_PAYORA_SIGNATURE'] ?? '';
$secret = getenv('PAYORA_WEBHOOK_SECRET');
$signedPayload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signedPayload, $secret);Ten przykład pokazuje jedynie ogólny schemat, a nie gwarantowany format. Jeśli Payora podpisuje wyłącznie treść, usuń znacznik czasu z ciągu. Jeśli podpis jest obliczany na podstawie kanonicznej postaci JSON-a, zastosuj dokładnie tę metodę zamiast własnej. Jeden błędny separator sprawi, że każde żądanie zostanie odrzucone.
Jeśli jako sprzedawca zwracasz uwagę także na zasady rozliczeń i sytuacje brzegowe, przydatnym uzupełnieniem będzie artykuł o limitach faktur za płatności kryptowalutowe. Obsługa webhooków i zasady wystawiania faktur często spotykają się na tym samym etapie realizacji zamówienia.
Porównaj obliczony podpis z podpisem Payora
Gdy masz już oczekiwany podpis, porównaj go z podpisem otrzymanym w żądaniu, używając porównania o stałym czasie wykonania. W PHP najczęściej oznacza to użycie funkcji hash_equals(). Zwykłe porównanie == lub === nie jest tu właściwym rozwiązaniem: może ujawniać informacje na podstawie czasu wykonania i gorzej radzi sobie z subtelnymi przypadkami niezgodności.
Przed porównaniem dopasuj format dokładnie do wymagań Payora. Jeśli nagłówek zawiera wartość szesnastkową, porównuj wartości szesnastkowe. Jeśli zawiera base64, porównuj wartości zakodowane w base64. Jeśli podpis ma prefiks, taki jak sha256=, usuń go albo uwzględnij zgodnie z dokumentacją.
Przykładowy przebieg:
- Odczytaj surową treść żądania.
- Odczytaj nagłówek z podpisem.
- Oblicz oczekiwany podpis według udokumentowanego wzoru.
- Porównaj oba ciągi za pomocą
hash_equals(). - Jeśli się nie zgadzają, odrzuć żądanie kodem 401 lub 400.
Kod 401 ma sens, gdy podpis jest nieprawidłowy. Kod 400 sprawdzi się, gdy brakuje wymaganych nagłówków. W obu przypadkach odpowiedź powinna być krótka. Nie zdradzaj żadnych dodatkowych informacji.
Warto zapamiętać jedno: nigdy nie wyświetlaj sekretu. To rzadki błąd, ale jego skutki mogą być katastrofalne.
Obsłuż znacznik czasu, ochronę przed powtórzeniami i dopuszczalny margines
Jeśli Payora dołącza znacznik czasu lub nonce, sprawdź go przed zaakceptowaniem zdarzenia. Zapobiega to atakom typu replay, w których atakujący ponownie wysyła prawidłowy webhook, próbując skłonić aplikację do podwójnego obciążenia płatnością lub ponownej wysyłki zamówienia. Zakres dopuszczalnego czasu powinien wynikać z zasad Payora, a nie z Twoich przypuszczeń.
Typowe rozwiązanie polega na porównaniu znacznika czasu webhooka z czasem serwera i odrzucaniu przeterminowanych żądań spoza dopuszczalnego przedziału. Jeśli dokumentacja podaje 5 minut, użyj 5 minut. Jeśli podaje 300 sekund, zapisz tę wartość jako nazwaną konfigurację i nie wpisuj niejasnej liczby na sztywno w trzech plikach.
Różnica czasu może spowodować odrzucenie prawidłowych żądań. Serwer, którego zegar spieszy się lub spóźnia o 7 minut, może odrzucać poprawne żądania. Synchronizuj serwery przez NTP i stosuj spójną konfigurację strefy czasowej. Strefa czasowa nie zmienia znaczników czasu Unix, ale nieprawidłowe ustawienia lokalne mogą utrudnić analizę logów.
Jeśli używane są nonce, obsługa jest jeszcze prostsza. Zapisz każdy nonce, oznacz go jako wykorzystany i odrzuć jego ponowne użycie. Wystarczy niewielka pamięć podręczna lub tabela w bazie danych. Jeden nonce — jedna akceptacja.
Jeśli na stronach płatności stosujesz też stałe ceny, znaczenie mają tu również przyjęte zasady. Osobny artykuł o tym, czy warto stosować stałą cenę, pomoże dopasować zasady kursów walut do czasu obsługi webhooków, by po opóźnionym zdarzeniu nie potwierdzić niewłaściwej kwoty.
Przetwarzaj prawidłowe zdarzenia i zwracaj odpowiedni kod HTTP
Po pomyślnej weryfikacji zdekoduj treść i skieruj ją do odpowiedniej obsługi w zależności od typu zdarzenia. Zdarzenie payment.succeeded nie powinno korzystać z tej samej ścieżki co refund.updated. Rozdziel je jasno. Dzięki temu każdy handler wykonuje jedno zadanie, a logi pozostają czytelne.
Logika biznesowa powinna zostać wykonana przed wysłaniem odpowiedzi z kodem 2xx. Jeśli oznaczasz zamówienie jako opłacone, najpierw zapisz zmianę w bazie danych. Jeśli wysyłasz e-mail przez kolejkę, najpierw dodaj do niej zadanie. Jeśli którykolwiek z tych kroków się nie powiedzie, zwróć kod 500, aby Payora mógł ponowić próbę później.
Kolejność ma znaczenie, ponieważ odpowiedź 2xx zwykle informuje Payora, że zdarzenie zostało przyjęte. Jeśli zbyt wcześnie zwrócisz 200, a potem wystąpi błąd przy zapisie do bazy, stan Payora i Twojej aplikacji może się rozjechać. Taki błąd potrafi słono kosztować o drugiej w nocy.
Wystarczy krótka treść odpowiedzi. Może to być „OK”. Jeszcze lepiej zwrócić pustą odpowiedź i jasno ustawić kod statusu. Nie odsyłaj treści żądania.
Zespołom, które na podstawie zdarzeń płatniczych wystawiają też faktury klientom, przyda się artykuł o bramce płatności kryptowalutowych dla freelancerów. Pokazuje, jak przejść od zdarzenia technicznego do rzeczywistej czynności rozliczeniowej bez zbędnych kroków.
Diagnozowanie nieudanej weryfikacji podpisu
Większość błędów wynika z jednego z pięciu powodów: zmienionej treści żądania, błędnej nazwy nagłówka, niewłaściwego sekretu, niezgodnego kodowania albo znacznika czasu spoza dopuszczalnego zakresu. Zacznij od treści. Jeśli zapisujesz surowy payload i obliczony podpis w logach, porównaj je, zanim zaczniesz sprawdzać bazę danych.
Zmiana treści jest częstym problemem. Oprogramowanie pośredniczące frameworka może usuwać białe znaki, dekodować JSON lub normalizować zakończenia wierszy. Wystarczy jeden dodatkowy znak nowej linii. Jeśli Payora podpisał {"a":1}, a aplikacja odczytuje {"a":1}\n, wartości HMAC nie będą zgodne.
Kolejnym częstym problemem jest niezgodność nagłówka. Niektóre serwery zamieniają łączniki na podkreślenia. Niektóre warstwy proxy usuwają niestandardowe nagłówki, o ile nie zostaną jawnie dozwolone. Jeśli PHP nie widzi nagłówka z podpisem, sprawdź konfigurację serwera WWW i proxy, a nie tylko kod.
Problemy z kodowaniem potrafią być podstępne. UTF-8 i znaki Unicode zapisane w postaci sekwencji ucieczki mogą wyglądać poprawnie w logach, a mimo to odpowiadać różnym bajtom. Jeśli aplikacja przepisuje JSON, podpis może przestać pasować, nawet gdy w przeglądarce treść wygląda identycznie.
Nieprawidłowy sekret to najprostsza przyczyna, ale często sprawdza się ją na końcu. Zweryfikuj osobno zmienną środowiskową, sekret środowiska testowego i sekret produkcyjny. Skopiowanie sekretu z niewłaściwego konta potrafi zmarnować całe popołudnie.
Różnica czasu powoduje inny objaw: podpis jest prawidłowy, ale żądanie i tak zostaje odrzucone, bo znacznik czasu jest zbyt stary. W takim przypadku sprawdź NTP, zegary kontenerów oraz to, czy jakiś reverse proxy nie dodaje własnego znacznika czasu. Niewielkie rozbieżności w infrastrukturze mogą wyglądać jak nieprawidłowy podpis.
Jeśli Twoja instalacja integruje się również z WordPressem lub systemem płatnego członkostwa, przydatny będzie artykuł o przyjmowaniu płatności w USDT. Pokazuje, jak obsługa webhooków, kontrola dostępu i potwierdzanie płatności współdziałają w ramach jednego procesu.
Przyda się jeszcze jeden test: wyślij dwukrotnie ten sam payload. Pierwsze żądanie powinno zostać zaakceptowane. Drugie powinno zostać odrzucone, jeśli zapisujesz i odrzucasz ponownie użyty nonce. Jeśli mechanizm ochrony przed powtórzeniami nie wykorzystuje nonce, drugie żądanie może zostać zaakceptowane. Taki test szybko pokaże, czy zabezpieczenie rzeczywiście działa.
Podsumowując, jak bezpiecznie obsługiwać webhooki płatnicze? Weryfikuj podpis na surowych danych, sprawdzaj znacznik czasu i ochronę przed powtórzeniami, a zdarzenia przetwarzaj dopiero po potwierdzeniu ich autentyczności.




Comments