Вебхуки: подписанные данные и логика повторов
Вы только что интегрировали платёжный шлюз. В документации сказано: «Мы отправим вебхук, когда платёж пройдёт успешно». Отлично. Но потом вы задумываетесь: как узнать, что запрос действительно пришёл от них? Что, если мой сервер недоступен в момент отправки? Что, если они отправят одно и то же событие дважды?
Вебхуки — мощный инструмент, но с подводными камнями. Это руководство разбирает механику, безопасность и паттерны надёжности, необходимые для корректной работы с ними.
Что такое вебхук?
Вебхук — это HTTP POST-запрос, который одна система отправляет другой при наступлении определённого события. Вместо того чтобы ваше приложение опрашивало API каждые несколько секунд («Есть новые заказы? Есть новые заказы?»), провайдер сам отправляет данные, как только что-то происходит.
Типичный сценарий:
- Вы регистрируете URL (например,
https://yourapp.com/webhooks/payments) у провайдера. - Происходит событие (платёж прошёл, пользователь зарегистрировался, файл загружен).
- Провайдер отправляет POST-запрос на ваш URL с JSON-телом, описывающим событие.
- Ваш сервер обрабатывает событие и отвечает
2xx, подтверждая получение.
Если вы не ответите быстро (обычно в течение 5–30 секунд), провайдер считает доставку неудачной и может повторить попытку.
Почему необходимо проверять подписанные данные
Ваш эндпоинт вебхука доступен публично. Любой, кто узнает URL, может отправлять поддельные события. Без проверки злоумышленник может сфабриковать событие «платёж прошёл» и получить товар бесплатно.
Решение: подписанные данные. Провайдер вычисляет криптографическую подпись тела запроса с использованием общего секрета (известного только вам и ему) и включает её в заголовок. Вы пересчитываете подпись на своей стороне и сравниваете.
Как работает подпись HMAC
Большинство провайдеров используют HMAC-SHA256. Шаги:
- Провайдер объединяет необработанное тело запроса с временной меткой (для защиты от атак повторного воспроизведения).
- Провайдер вычисляет
HMAC-SHA256(secret, timestamp + '.' + body). - Провайдер отправляет подпись в заголовке, например:
X-Signature: t=1234567890,v1=abc123.... - Вы извлекаете временную метку и подпись, пересчитываете HMAC, используя свою копию секрета, и сравниваете с помощью функции сравнения с постоянным временем.
Вот минимальный пример на Node.js:
const crypto = require('crypto');
function verifySignature(req, secret) {
const signatureHeader = req.headers['x-signature'];
const [tPart, vPart] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const receivedSig = vPart.split('=')[1];
const payload = timestamp + '.' + JSON.stringify(req.body);
const expectedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Сравнение с постоянным временем для защиты от timing-атак
return crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expectedSig, 'hex')
);
}
Важно: Всегда используйте необработанное тело запроса для проверки подписи. Если ваш фреймворк парсит JSON и повторно сериализует его, последовательность байтов может отличаться, что приведёт к ошибке проверки. Захватывайте необработанное тело до парсинга.
Проверка временной метки
Убедитесь, что временная метка свежая (например, в пределах 5 минут). Это предотвращает атаки повторного воспроизведения, когда злоумышленник перехватывает валидный запрос и отправляет его позже.
Логика повторов: изящная обработка сбоев
Ваш сервер может быть недоступен, перегружен или испытывать сбои сети. Провайдеры справляются с этим, повторяя неудачные доставки. Но вы должны проектировать свой эндпоинт так, чтобы безопасно обрабатывать повторы.
Экспоненциальная задержка с джиттером
Провайдеры обычно повторяют с увеличивающимися интервалами: 1 минута, 5 минут, 30 минут, 2 часа и т.д. Это называется экспоненциальной задержкой. Некоторые добавляют случайный джиттер, чтобы избежать проблемы «стада».
С вашей стороны вы не контролируете расписание повторов провайдера, но вы должны:
- Отвечать
2xxсразу после того, как безопасно сохранили событие (даже если обработка асинхронна). - Возвращать
4xxдля постоянных ошибок (например, некорректные данные), чтобы провайдер прекратил повторы. - Возвращать
5xxдля временных сбоев (например, база данных недоступна), чтобы вызвать повтор.
Идемпотентность: ключ к безопасным повторам
Повторы означают, что вы можете получить одно и то же событие несколько раз. Ваша обработка должна быть идемпотентной — обработка одного и того же события дважды должна давать тот же результат, что и однократная.
Распространённый подход: включить уникальный ID события в данные. Перед обработкой проверьте, не обрабатывали ли вы уже этот ID. Если да, верните 200 и пропустите.
async function handleWebhook(event) {
const existing = await db.events.findOne({ id: event.id });
if (existing) {
return { status: 200, body: 'Already processed' };
}
await db.events.insert({ id: event.id, processedAt: new Date() });
await processEvent(event);
return { status: 200, body: 'OK' };
}
Этот паттерн гарантирует, что даже если провайдер отправит событие трижды, вы выполните действие только один раз.
Сравнение: опрос vs вебхуки
| Аспект | Опрос | Вебхуки |
|---|---|---|
| Задержка | Зависит от интервала (например, 30 с) | Почти реальное время |
| Нагрузка на сервер | Постоянная, даже без событий | Только при событиях |
| Сложность | Просто реализовать | Требует эндпоинт, проверку, повторы |
| Надёжность | Вы контролируете повторы | Зависит от политики повторов провайдера |
Вебхуки выигрывают для обновлений в реальном времени, но опрос может быть запасным вариантом для критичных событий, если вы беспокоитесь о пропущенных доставках.
Чек-лист лучших практик
- Всегда проверяйте подписи. Никогда не доверяйте непроверенному вебхуку.
- Используйте HTTPS. Данные вебхуков часто содержат конфиденциальную информацию.
- Отвечайте быстро. Подтверждайте получение сразу, затем обрабатывайте асинхронно при необходимости.
- Реализуйте идемпотентность. Сохраняйте ID событий, чтобы избежать дублирующей обработки.
- Логируйте всё. Ведите запись полученных событий для отладки и аудита.
- Мониторьте сбои. Настройте оповещения о повторяющихся ответах 5xx или ошибках проверки подписи.
- Тестируйте с реальными данными. Используйте тестовый режим провайдера или отправляйте образцы событий на ваш эндпоинт.
Отладка проблем с вебхуками
Когда вебхуки не работают, первый шаг — исследовать необработанный запрос. Частые проблемы:
- Несовпадение подписи: вы используете распарсенное тело вместо необработанного. Исправьте, захватывая необработанное тело.
- Тайм-аут: ваш эндпоинт слишком долго отвечает. Перенесите тяжёлую обработку в фоновую задачу.
- Пропущенные события: проверьте панель управления провайдера на предмет журналов доставки и истории повторов.
Если вы отлаживаете HTTP-трафик, такие инструменты, как Nginx Log Analyzer, помогут выявить закономерности в сбоях запросов и кодах ответов.
FAQ
Что такое секрет вебхука и как его хранить?
Секрет — это общий ключ, используемый для подписи данных. Храните его в переменных окружения или менеджере секретов — никогда в коде или системе контроля версий. Периодически меняйте его, если провайдер поддерживает такую возможность.
Сколько раз провайдер повторит неудачный вебхук?
Зависит от провайдера. Некоторые повторяют в течение 24 часов с экспоненциальной задержкой, другие — 3 дня. Проверьте документацию вашего провайдера для точного расписания и максимального числа попыток.
Можно ли использовать вебхуки с бессерверными функциями?
Да, но учитывайте ограничения времени выполнения. Подтверждайте вебхук быстро (возвращайте 200) и переносите тяжёлую обработку в очередь или фоновую функцию, чтобы избежать тайм-аутов.
Готовы создавать надёжные интеграции с вебхуками? Начните с проверки подписей и реализации идемпотентности — эти два шага сэкономят вам часы отладки и предотвратят дорогостоящие инциденты безопасности.