Вебхуки: подписанные payload и повторные попытки
Вы только что интегрировали платёжный шлюз. В документации сказано: «Мы отправим POST-запрос на ваш эндпоинт, когда платёж пройдёт успешно». Легко, правда? Вы создаёте простой маршрут, который принимает JSON, обновляет базу данных и возвращает 200. И тут случается: платёж клиента проходит успешно, но ваш сервер на мгновение недоступен. Вебхук так и не приходит. Или хуже — приходит дважды, и вы дважды зачисляете сумму на счёт. Вебхуки кажутся простыми, пока не начинают сбоить.
В этой статье объясняется, как на самом деле работают вебхуки, почему подписанные payload критически важны для безопасности и как построить логику повторных попыток, которая обеспечит надёжность вашей интеграции — даже когда что-то идёт не так.
Что такое вебхук?
Вебхук — это HTTP-колбэк: вместо того чтобы ваше приложение опрашивало API на предмет изменений, провайдер отправляет HTTP POST на указанный вами URL при наступлении события. Это push вместо pull.
Типичные примеры: подтверждения платежей (Stripe, PayPal), уведомления о сборках CI/CD (GitHub, GitLab) и платформы обмена сообщениями (Slack, Discord). Схема проста: вы регистрируете URL, провайдер отправляет JSON-payload на этот URL, а ваш сервер их обрабатывает.
Но за этой простотой скрываются две критически важные задачи: подлинность (как узнать, что запрос действительно пришёл от провайдера?) и надёжность (что произойдёт, если ваш эндпоинт недоступен или медленно отвечает?).
Почему подписанные payload важны
Ваш эндпоинт вебхука — это публичный URL. Любой может отправить на него POST-запрос. Без верификации злоумышленник может подделать событие успешного платежа, инициировать отправку товара или испортить ваши данные.
Подписанные payload решают эту проблему, включая криптографическую подпись в заголовках запроса. Провайдер вычисляет HMAC (код аутентификации сообщения на основе хеша), используя общий секрет и тело запроса, а затем отправляет его вместе с payload. Ваш сервер пересчитывает HMAC и сравнивает. Если они совпадают — запрос подлинный.
Как работает HMAC-подпись
- Провайдер и вы используете общий секретный ключ (предоставляется при настройке).
- Для каждого вебхука провайдер конкатенирует временную метку и исходное тело запроса, затем вычисляет хеш HMAC-SHA256 с использованием секрета.
- Подпись отправляется в заголовке, часто с именем вроде
X-SignatureилиStripe-Signature. - Ваш сервер пересчитывает HMAC с тем же секретом и сравнивает с полученной подписью.
- Если они совпадают, вы обрабатываете событие; иначе отклоняете с кодом 401.
Всегда используйте исходное тело запроса для вычисления HMAC. Если ваш фреймворк парсит JSON до верификации, тело может быть изменено (пробелы, порядок ключей), что приведёт к ошибке верификации.
Пример: проверка подписи вебхука в Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload.toString());
// ... handle event
res.status(200).send('OK');
});
Обратите внимание на использование timingSafeEqual для предотвращения атак по времени. Никогда не используйте простое сравнение === для подписей.
Проектирование логики повторных попыток
Даже с подписями вебхуки могут давать сбои. Ваш сервер может быть недоступен, сеть может потерять запрос, или обработка может завершиться по таймауту. Провайдеры обычно повторяют неудачные доставки, но вы также должны проектировать систему для корректной обработки повторных попыток.
Что считается сбоем?
Большинство провайдеров считают сбоем любой ответ, кроме 2xx. Это включает 3xx-редиректы, 4xx-ошибки клиента и 5xx-ошибки сервера. Некоторые также устанавливают таймаут (например, 5 секунд). Если вы не отвечаете быстро, доставка помечается как неудачная и повторяется.
Экспоненциальная задержка с джиттером
Провайдеры обычно повторяют с экспоненциальной задержкой: первая повторная попытка через 1 минуту, затем через 5 минут, 30 минут и т.д. Добавление джиттера (случайного разброса) предотвращает эффект «стада» (thundering herd), если множество вебхуков сбоят одновременно.
Если вы создаёте отправитель вебхуков, реализуйте задержку следующим образом:
- Повтор 1: через 10 секунд
- Повтор 2: через 1 минуту
- Повтор 3: через 5 минут
- Повтор 4: через 30 минут
- Повтор 5: через 2 часа
После максимального числа попыток переместите событие в очередь мёртвых писем (dead-letter queue) для ручной проверки.
Идемпотентность: обработка дублирующихся доставок
Повторные попытки означают, что ваш эндпоинт может получить одно и то же событие несколько раз. Ваша обработка должна быть идемпотентной: обработка одного и того же события дважды должна давать тот же эффект, что и однократная обработка.
Распространённые стратегии:
- Дедупликация по ID события: сохраняйте обработанные ID событий в базе данных или кэше. Перед обработкой проверьте, не встречался ли этот ID ранее.
- Upsert вместо insert: используйте операции с базой данных, которые естественно обрабатывают дубликаты (например,
INSERT ... ON CONFLICT DO NOTHING). - Ключи идемпотентности: для операций вроде списания с карты передавайте уникальный ключ, чтобы платёжный провайдер игнорировал дублирующиеся запросы.
Лучшие практики для потребителей вебхуков
| Практика | Почему это важно |
|---|---|
| Проверяйте подписи | Предотвращает подделку запросов |
| Отвечайте быстро (в течение 2 с) | Избегает таймаутов и лишних повторных попыток |
| Обрабатывайте асинхронно | Разделяет приём и тяжёлую обработку |
| Используйте идемпотентные обработчики | Безопасность при дублирующихся доставках |
| Логируйте все события | Помогает при отладке и аудите |
| Мониторьте сбои | Обнаруживайте проблемы до их эскалации |
Также рассмотрите валидацию временной метки в подписи для предотвращения атак повторного воспроизведения. Многие провайдеры включают временную метку в подписанный payload; отклоняйте события старше нескольких минут.
Лучшие практики для отправителей вебхуков
Если вы создаёте сервис, отправляющий вебхуки, следуйте этим рекомендациям:
- Подписывайте каждый payload с помощью HMAC-SHA256.
- Включайте уникальный ID события и временную метку.
- Повторяйте с экспоненциальной задержкой и джиттером.
- Предоставьте потребителям возможность просматривать логи доставки и вручную повторять события.
- Чётко документируйте процесс проверки подписи.
FAQ
Что такое подписанный payload вебхука?
Подписанный payload включает криптографическую подпись (обычно HMAC), вычисленную из тела запроса и общего секрета. Получатель проверяет подпись, чтобы убедиться, что запрос подлинный и не изменён.
Сколько раз следует повторять вебхук?
Универсального числа нет, но большинство провайдеров повторяют 3–5 раз в течение нескольких часов. После этого событие обычно помечается как неудачное и может быть доступно для ручного воспроизведения. Ваша система должна обрабатывать дубликаты идемпотентно независимо от количества повторных попыток.
Можно ли использовать вебхуки без HTTPS?
Технически да, но всегда следует использовать HTTPS. Вебхуки часто содержат конфиденциальные данные, и без TLS злоумышленник может перехватить или подменить payload. HTTPS также гарантирует, что вы отправляете данные на нужный сервер.
Отладка вебхуков с помощью логов Nginx
Когда вебхуки дают сбои, первое место для поиска — логи вашего веб-сервера. Если вы используете Nginx, логи доступа и ошибок фиксируют каждый запрос, включая коды статуса и время ответа. Вы можете быстро заметить закономерности: повторяющиеся 401 (ошибка проверки подписи), 500 (ошибки приложения) или таймауты.
Для более быстрого анализа попробуйте Nginx Log Analyzer, чтобы разобрать и обобщить трафик вебхуков, отфильтровать по коду статуса и выявить проблемные эндпоинты.