Что такое подписанные вебхуки Payora
Подписанные вебхуки Payora — это HTTP-запросы с цифровой подписью, благодаря которой ваш PHP-эндпоинт может проверить, что событие действительно пришло от Payora и не было изменено по пути. Если вы разбираетесь, как проверить вебхук Payora в PHP и понимаете, что такое проверка подписанного вебхука Payora PHP, базовый процесс прост: Payora отправляет событие, ваш сервер читает исходное тело запроса, а ваш код проверяет подпись, прежде чем доверять чему-либо в содержимом.
Такая проверка важна, потому что вебхук может прийти от любого клиента, который знает ваш URL. Злоумышленник может взять адрес из логов, а затем отправить поддельное событие о платеже с аккуратным JSON-телом. Подпись ломает этот трюк.
Само событие полезно только после проверки. До этого это просто текст.
Представьте себе пронумерованный билет на входе. Тело запроса — это сообщение, подпись — это штамп, а ваш PHP-код решает, совпадают ли обе части. Если да, вы обрабатываете событие. Если нет, быстро отклоняете его.
Если у вас уже настроен платёжный поток на сайте, та же дисциплина проявляется и в других частях стека. Предприниматель, который читает руководство по криптоплатёжному шлюзу для e-commerce, увидит тот же принцип: меньше принимать на веру, больше проверять и только потом обновлять заказ.
Что нужно для PHP-эндпоинта вебхука
Для этого не нужен огромный фреймворк. Достаточно PHP-эндпоинта на PHP 8.x, а для некоторых проектов может подойти и PHP 7.4, но код должен иметь доступ к сырому телу запроса и к заголовкам, которые отправляет Payora. Это значит, что ваша серверная конфигурация должна обеспечивать обычный доступ к HTTP-запросу, а не использовать слой, который переписывает или отбрасывает заголовки.
Используйте HTTPS. Без TLS вебхук передаётся в открытом виде, и одна подпись не спасает слабую защиту канала. Сертификат, корректная настройка виртуального хоста и доступный публичный URL — это минимальная база.
На практике важны два расширения PHP: hash для работы с HMAC и, если вы строите обработку JSON вокруг него, стандартные JSON-функции, которые идут вместе с PHP. Для тестирования со своей стороны вам также может пригодиться cURL, но самому эндпоинту вебхука он не нужен.
Также вам нужен секрет Payora или ключ подписи, который соответствует настройкам вебхука в вашем аккаунте Payora. Храните это значение вне web root. Не коммитьте его в git. Секрет в публичном репозитории быстро превращает проверку в театр.
И ещё один момент: ваш эндпоинт должен принимать только POST-запросы. На GET можно отвечать 405, и это небольшое ограничение экономит время при отладке, потому что сразу сужает круг трафика, который вы реально проверяете.
Снимите сырую полезную нагрузку вебхука и заголовки
Проверка подписи зависит от точных байтов, которые отправил Payora. В PHP читайте сырое тело запроса через file_get_contents('php://input'). Не собирайте полезную нагрузку заново из $_POST или из декодированного JSON, потому что любой из этих шагов может изменить пробелы, порядок ключей или кодировку.
То же правило относится и к заголовкам. Вам нужен заголовок подписи. Используйте getallheaders(), если он доступен, или читайте серверные переменные вроде $_SERVER['HTTP_X_...'], если ваше окружение не предоставляет этот хелпер. Небольшие различия у хостинга здесь имеют значение.
Представьте тело вебхука размером 412 байт. Если ваш код декодирует его и потом кодирует снова, поля могут выглядеть одинаково, но байты уже не будут идентичны. Одного такого несовпадения достаточно, чтобы сломать проверку.
Привычка должна быть такой: сначала захватить, потом разобрать. Сначала сохраните сырую строку. Затем изучите заголовки. И только после этого трогайте структуру полезной нагрузки.
Если вы тестируете платёжные потоки до запуска, тот же подход экономит время и в других местах. Статья о как протестировать криптоплатёж применяет ту же логику к sandbox-событиям, где точность полезной нагрузки и делает тест полезным.
Воссоздайте ожидаемую подпись в PHP
Обычно используется HMAC, вычисленный на основе сырой полезной нагрузки и общего секрета. Начните с алгоритма, указанного в документации Payora по вебхукам. Если в документации указан SHA-256, используйте SHA-256. Если там есть префикс, версия или формат временной метки, включайте именно эту структуру.
Обычно схема выглядит так: сформируйте подписываемое сообщение из временной метки и сырого тела, затем запустите hash_hmac() с общим секретом и указанным алгоритмом хеширования. В зависимости от спецификации результат может потребовать шестнадцатеричного или base64-кодирования. Не угадывайте формат. Смотрите его в настройках вебхука.
Ниже минимальный пример в стиле PHP, с заглушками там, где формат Payora может отличаться:
$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);Этот пример — только схема, а не обещание. Если Payora подписывает только само тело, уберите временную метку из строки. Если Payora подписывает каноническую JSON-форму, следуйте именно этому методу, а не тому, который вам удобнее. Один неверный разделитель сломает каждый запрос.
Для продавцов, которым важны также правила выставления счетов и пограничные случаи, полезной будет статья о лимитах инвойсов для криптоплатежей, потому что обработка вебхуков и политика выставления счетов часто встречаются в одном и том же жизненном цикле заказа.
Сравните вычисленную подпись с подписью Payora
Когда у вас есть ожидаемая подпись, сравнивайте её с входящей подписью с помощью сравнения за постоянное время. В PHP для этого обычно используют hash_equals(). Простые проверки через == или === здесь не подходят, потому что они могут раскрывать временные подсказки и хуже защищают от тонких случаев несовпадения.
Перед сравнением нормализуйте формат ровно так, как ожидает Payora. Если заголовок в hex, сравнивайте hex. Если заголовок в base64, сравнивайте base64. Если входящая подпись содержит префикс вроде sha256=, уберите его или оставьте именно так, как сказано в документации.
Пример потока:
- Считать сырое тело.
- Считать заголовок подписи.
- Собрать ожидаемую подпись по описанной в документации формуле.
- Сравнить обе строки через
hash_equals(). - Отклонить запрос с 401 или 400, если они не совпадают.
401 уместен, когда подпись неверна. 400 уместен, когда обязательные заголовки отсутствуют. В любом случае ответ должен быть коротким. Без лишних подсказок.
Одного предложения здесь достаточно: не выводите секрет. Эта ошибка редкая, но катастрофическая.
Обрабатывайте timestamp, защиту от повторов и правила допуска
Если Payora включает временную метку или nonce, проверяйте их до принятия события. Это блокирует replay-атаки, когда злоумышленник повторно отправляет валидный вебхук позже и пытается заставить приложение дважды заплатить или дважды отправить товар. Окно по времени должно соответствовать правилу Payora, а не вашему предположению.
Обычно проверка выглядит так: сравните timestamp вебхука со временем сервера и отклоните устаревшие запросы вне окна допуска. Если в документации сказано 5 минут, используйте 5 минут. Если там указано 300 секунд, сохраните 300 секунд как именованную настройку и не захардкоживайте загадочное число в трёх файлах.
Смещение часов может ломать хороший трафик. Сервер, который отстаёт на 7 минут, может отклонить валидный запрос. Синхронизируйте серверы через NTP и следите за единым часовым поясом. Часовой пояс не меняет Unix timestamps, но неверные локальные настройки всё равно могут сделать логи труднее для чтения.
Nonce ещё проще, если они есть. Сохраните каждый nonce один раз, отметьте его как использованный и отклоняйте дубликат. Для этого подойдёт небольшой кэш или таблица в базе данных. Один nonce — одно принятие.
Если вы также используете фиксированную цену на страницах оплаты, здесь важна политика. Отдельная статья о стоит ли использовать фиксированную цену поможет согласовать правила курса с таймингом вебхука, чтобы вы не подтвердили неверную сумму после позднего события.
Обрабатывайте валидные события и возвращайте правильный HTTP-ответ
После успешной проверки разберите полезную нагрузку и разделите логику по типу события. payment.succeeded не должен идти по тому же пути, что и refund.updated. Разделяйте их чисто. Тогда у каждого обработчика будет одна задача, а логи останутся читаемыми.
Бизнес-логика должна отработать до отправки ответа 2xx. Если вы отмечаете заказ как оплаченный, сначала запишите строку в базу. Если вы ставите письмо в очередь, сначала добавьте задачу в очередь. Если любой из этих шагов не удаётся, верните 500 и дайте Payora повторить попытку позже.
Это важно, потому что ответ 2xx обычно сообщает Payora, что событие принято. Если вы вернёте 200 слишком рано, а затем упадёте на записи в базу, может возникнуть расхождение между Payora и вашим приложением. Такой баг дорогой в 2 часа ночи.
Небольшого тела ответа достаточно. Подойдёт “OK”. Ещё лучше — не возвращать тело вовсе и оставить только понятный статус-код. Не возвращайте обратно полезную нагрузку.
Для команд, которые также выставляют клиентам счета на основе платёжных событий, статья о криптоплатёжном шлюзе для фрилансеров показывает, как вебхук может перейти от технического события к реальному выставлению счёта без лишних шагов.
Устранение проблем с проверкой подписи
Большинство сбоев возникает из пяти причин: изменилось сырое тело, неверно указано имя заголовка, неверный секрет, отличается кодировка или проверка timestamp выходит за пределы допуска. Начните с тела. Если вы логировали сырую полезную нагрузку и вычисленную подпись, сравните их до обращения к базе данных.
Изменение тела — частая проблема. Промежуточное ПО фреймворка может обрезать пробелы, декодировать JSON или нормализовать окончания строк. Одной лишней новой строки уже достаточно. Если Payora подписал {"a":1}, а ваше приложение читает {"a":1}\n, HMAC не совпадёт.
Следующий пункт — несовпадение заголовков. Некоторые серверы меняют дефисы на подчёркивания. Некоторые прокси-слои удаляют пользовательские заголовки, если их явно не разрешить. Если в PHP не видно заголовка подписи, проверьте конфиг веб-сервера и настройки прокси, а не только код.
Проблемы кодировки коварны. UTF-8 и экранированный Unicode могут нормально выглядеть в логах и при этом давать разные байты. Если приложение переписывает JSON, подпись может сломаться, хотя полезная нагрузка в браузере выглядит одинаково.
Неверный секрет — самая простая ошибка и та, которую проверяют последней. Отдельно проверьте переменную окружения, секрет для staging и секрет для production. Скопированный секрет из другого аккаунта может украсть весь ваш день.
Смещение часов даёт другой симптом: подпись совпадает, но запрос всё равно отклоняется, потому что timestamp слишком старый. Если так случилось, проверьте NTP, часы контейнеров и любой reverse proxy, который ставит своё время. Небольшой инфраструктурный дрейф может выглядеть как плохая подпись.
Если ваша установка также связана с WordPress или членскими платежами, статья о как принимать платежи в USDT будет полезна, потому что показывает, как обработка вебхуков, контроль доступа и подтверждение оплаты связываются в один поток.
Один финальный тест очень помогает: отправьте один и тот же payload дважды. Первый запрос должен пройти. Второй должен не пройти, если вы храните nonce и отклоняете повторное использование, или должен пройти только в том случае, если ваша схема вебхука вообще не использует защиту от повторов на основе nonce. Этот один дубль показывает, действительно ли защита работает.




Комментарии