MORLIX JOURNAL
Вебхуки: как принимать события и проверять подпись
Как безопасно принимать вебхуки: проверка HMAC, исходное тело запроса, защита от повторов, очередь и пример на PHP.
Вебхук — это HTTP-запрос, которым одна система сообщает другой о событии: например, об изменении заказа или завершении операции. Чтобы безопасно принимать такие сообщения, обработчик должен проверить подлинность запроса, защититься от повторного выполнения и сохранить событие до подтверждения приёма.
Ниже — практическая схема для разработчика сайта или интеграции: как проверить подпись вебхука на неизменённом теле запроса, обработать повторную доставку и не потерять событие при сбое. Пример подписи и заголовков условный: у конкретного провайдера формат, алгоритм и правила ответов могут отличаться.
Как работает вебхук и чем он отличается от обычного запроса
При периодическом опросе получатель сам спрашивает удалённый сервис, появились ли изменения. В модели вебхуков отправитель инициирует запрос на заранее настроенный адрес получателя, когда возникает событие. В теле часто передают тип события, идентификатор и сведения об объекте. Получатель отвечает HTTP-статусом, а подробные правила повторов и тайм-аутов задаёт отправитель.
Пример: платёжный сервис уведомляет магазин об изменении состояния операции. Сетевой запрос может прийти с задержкой, повториться после тайм-аута или оказаться доставленным после более нового события. Поэтому вебхук следует воспринимать как уведомление, требующее проверки и устойчивой обработки, а не как команду, которую можно безусловно выполнить.
В отличие от клиентского обращения к REST API направление запроса здесь обратное: внешний сервис обращается к вашему серверу. Общие принципы маршрутов и авторизации разобраны в статье о REST API для CMS.
Безопасная последовательность обработки
- Примите запрос по HTTPS на выделенный маршрут; ограничьте допустимый метод, размер тела и время обработки.
- Прочитайте исходные байты тела и заголовки, необходимые по протоколу поставщика.
- Проверьте подпись и, если она предусмотрена и защищена подписью, допустимое время сообщения.
- После успешной проверки разберите данные, проверьте обязательные поля, тип события и принадлежность нужной учётной записи или окружению.
- Атомарно сохраните идентификатор события и задание на обработку. Дубликат распознайте по устойчивому идентификатору, а не по времени прихода.
- Верните согласованный успешный ответ только после надёжного сохранения; тяжёлую работу выполните асинхронно.
Сначала изучите документацию конкретного отправителя: он определяет точную строку для подписи, названия заголовков, способ кодирования, возможные версии ключей, тайм-ауты и поведение повторов. Нельзя считать приведённую ниже схему универсальным протоколом для всех сервисов.

Что доказывает проверка подписи вебхука
Один из распространённых вариантов — HMAC: отправитель и получатель знают общий секрет. Отправитель вычисляет код аутентификации по согласованным данным и передаёт результат вместе с запросом. Получатель вычисляет HMAC заново и сравнивает его с присланным значением. При правильной реализации совпадение подтверждает целостность подписанных данных и то, что сообщение сформировано стороной, знающей секрет.
HMAC не шифрует тело и не является асимметричной цифровой подписью. Для защиты передачи используют HTTPS. Проверка также не делает все поля безопасными: после неё нужны проверка схемы и бизнес-ограничений, а при необходимости — сверка актуального состояния через авторизованный API поставщика.
Подписывайте именно те байты, которые требует протокол. Если отправитель подписал исходный JSON, повторное кодирование разобранного объекта может изменить пробелы, порядок полей или экранирование и дать другую подпись. Сначала сохраните исходные байты в памяти, затем проверяйте подпись; JSON разбирайте после успешной проверки.
Пример: проверка HMAC-SHA256 на PHP
Предположим, что для учебной интеграции стороны согласовали такой формат: заголовок X-Hook-Timestamp содержит время Unix в секундах, X-Hook-Signature — 64 строчных шестнадцатеричных символа HMAC-SHA256, а подписываемые данные состоят из строки времени, точки и исходных байтов тела. Время включено в подписываемую строку; неподписанному времени доверять нельзя. Это вымышленное соглашение, а не формат конкретного провайдера.
Функция ниже проверяет подпись и окно в пять минут для этой схемы. Секрет берётся из защищённой конфигурации; если он отсутствует, интеграция должна завершиться ошибкой, а не принимать запрос без проверки.
<?php
declare(strict_types=1);
function verifyExampleWebhook(
string $rawBody,
string $timestamp,
string $providedSignature,
string $secret,
int $now
): bool {
if ($secret === '') {
return false;
}
if (!preg_match('/\A[0-9]{10}\z/', $timestamp) ||
!preg_match('/\A[0-9a-f]{64}\z/', $providedSignature)) {
return false;
}
$eventTime = (int) $timestamp;
if ($eventTime < $now - 300 || $eventTime > $now + 300) {
return false;
}
$signed = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signed, $secret);
return hash_equals($expected, $providedSignature);
}
Получатель вызывает функцию с исходным телом запроса, значениями двух заголовков, секретом и текущим временем сервера. hash_hmac в этом примере возвращает шестнадцатеричную строку; hash_equals предназначен для безопасного сравнения строк. Проверка формата заранее ограничивает принимаемое представление подписи. Часы сервера должны быть синхронизированы, иначе корректные сообщения могут отклоняться.
Это только проверяющая функция, а не законченный HTTP-обработчик: она не читает запрос, не хранит событие и не отправляет ответ. Для другого поставщика необходимо в точности повторить его алгоритм, кодировку подписи, порядок полей, правила нескольких подписей и ротации секретов. Некоторые сервисы подписывают только тело либо используют асимметричные ключи; приведённый код им не подойдёт.
Как прочитать тело, не меняя его
В обработчике PHP исходное тело обычно получают из php://input. До разбора JSON прочитайте ограниченное число байтов: например, если лимит приложения — 1 МиБ, прочитайте максимум на один байт больше и отклоните превышение. Сетевой сервер и прокси тоже должны ограничивать размер и длительность запроса. Не полагайтесь только на заявленный клиентом заголовок размера: проверяйте фактически прочитанные данные.
Если тело не прочитано, секрет недоступен или подпись неверна, не запускайте бизнес-обработку. После проверки используйте разбор JSON с обработкой ошибок и валидируйте структуру и типы. Содержимое нельзя напрямую подставлять в SQL, HTML или команды системы: применяйте параметризованные запросы, контекстное экранирование и другие меры, описанные в статье о защите веб-приложений.

Повторы, replay и идемпотентность
Даже корректный отправитель может повторить доставку, если не получил подтверждение вовремя. Проверка HMAC сама по себе не отличает повтор подлинного сообщения от первой доставки. Подписанная временная метка ограничивает возраст принимаемого сообщения, но в пределах окна тот же запрос всё ещё можно воспроизвести.
Храните устойчивый идентификатор события после проверки подписи; убедитесь, что он входит в подписанные данные или иным способом надёжно связан с ними. Ограничение уникальности в базе должно учитывать отправителя и окружение, если идентификаторы не гарантированно уникальны между ними. В одной транзакции создавайте запись принятого события и задание в таблице той же базы. Если очередь — отдельный брокер, используйте транзакционный outbox или другой проверенный механизм надёжной передачи. При конфликте уникальности повторно не выполняйте бизнес-действие; ответьте согласно контракту отправителя.
Обычной проверки «сначала поискать ID, потом вставить» недостаточно при параллельных запросах: оба процесса могут не найти запись и выполнить действие дважды. Нужны атомарное ограничение уникальности и корректная обработка конфликта. Для внешних действий используйте идемпотентный ключ получателя, если тот его поддерживает, или отдельный механизм согласования: транзакция вашей базы не делает атомарным удалённый API-вызов.
Срок хранения идентификаторов выбирайте с учётом окна повторов поставщика и бизнес-рисков. Не отбрасывайте легитимные запоздавшие события без понимания контракта: одни отправители переотправляют событие с новой подписью и временем, другие работают иначе.
Ответы, очередь и порядок событий
Не выполняйте долгие операции до ответа отправителю, если из-за этого можно превысить его тайм-аут. После проверки быстро сохраните событие и задание в устойчивое хранилище, подтвердите транзакцию и только затем верните успешный код, который признаёт поставщик. Если сохранение не удалось, не сообщайте об успехе: выберите ответ по контракту сервиса, чтобы не потерять событие.
Отдельный обработчик очереди может обновить данные, отправить уведомление или синхронизировать заказ. При временном сбое используйте ограниченные повторы с задержкой и механизм разбора окончательно неудачных заданий. Даже при устойчивой очереди обработчик должен выдерживать повторное выполнение: процесс может завершиться после внешнего действия, но до фиксации результата у вас.
Не предполагайте, что сообщения придут строго по порядку. Сравнивайте версии или время изменения объекта, если протокол надёжно их предоставляет; при сомнении запрашивайте актуальное состояние у поставщика через авторизованный API. Для чувствительных операций проверяйте, что идентификатор объекта и учётная запись относятся к вашей интеграции.
Секреты и журналирование
Выдавайте отдельные секреты для разных интеграций и окружений, храните их в защищённой конфигурации и ограничивайте доступ к ним. Не вставляйте секрет в репозиторий, URL, клиентский код или примеры логов. Запланируйте замену: в переходный период проверка может поддерживать старый и новый ключи, если это допускает протокол поставщика; после завершения перехода старый ключ нужно отозвать.
Для диагностики сохраняйте идентификатор события, тип, время приёма, результат проверки и обработки, счётчик попыток и безопасный идентификатор корреляции. Не записывайте секреты, полное тело или персональные данные без необходимости. Ограничьте срок хранения и доступ к журналам. Ограничение частоты запросов и список доверенных адресов, если он доступен и поддерживается в актуальном состоянии, могут дополнять проверку подписи, но не заменяют её.
Что проверить перед запуском
- Корректное подписанное сообщение принимается; изменение одного байта тела или подписи приводит к отказу.
- Отсутствующие и некорректные заголовки, неизвестный ключ и слишком большое тело безопасно отклоняются.
- Для подписанной временной метки проверены устаревшее и будущее время, а также погрешность часов.
- Два параллельных запроса с одним ID не запускают бизнес-действие дважды.
- Сбой базы до фиксации не возвращает ложное подтверждение; сбой работника после фиксации можно восстановить.
- Проверены повторная доставка, нарушение порядка, ротация секретов и раздельные тестовое и рабочее окружения.
- Логи не содержат секретов; есть оповещение о росте ошибок проверки, очереди и неудачных обработок.
Проведите проверки в тестовом окружении с примерами реального поставщика и его средствами повторной отправки. Затем включите обработчик на ограниченном наборе событий и наблюдайте за очередью и ошибками. Общие проверки инфраструктуры собраны в чек-листе перед запуском сайта.