Вебхуки: подписанные данные и повторные попытки
Вы только что интегрировали стороннего платёжного провайдера, и он обещает уведомлять ваше приложение об успешном платеже через вебхук. Но откуда вы знаете, что запрос действительно пришёл от него, а не от злоумышленника? И что произойдёт, если ваш сервер будет недоступен в момент отправки вебхука? Без надлежащих мер защиты вы рискуете обработать поддельные события или потерять критически важные обновления. В этой статье объясняется, как защитить вебхуки с помощью подписанных данных и реализовать надёжные механизмы повторных попыток.
Что такое вебхук?
Вебхук — это определяемый пользователем HTTP-колбэк. Когда в исходной системе происходит событие (например, завершается платёж), она отправляет HTTP POST-запрос на указанный вами URL. В отличие от опроса (polling), при котором вы многократно запрашиваете обновления, вебхуки передают данные вам почти в реальном времени. Они широко используются для уведомлений о событиях, таких как триггеры CI/CD, сообщения в чатах или изменения статуса платежа.
Зачем защищать вебхуки подписями?
Конечные точки вебхуков общедоступны, поэтому любой может отправить на них POST-запрос. Без проверки злоумышленник может подделать события, что приведёт к несанкционированным действиям, например к отметке заказа как оплаченного. Подписание данных общим секретом позволяет проверить подлинность и целостность.
Как работают подписанные данные
Отправитель и получатель используют общий секретный ключ. Для каждого вебхука отправитель вычисляет криптографическую подпись (обычно HMAC) по телу запроса и включает её в заголовок. Получатель пересчитывает подпись с тем же секретом и сравнивает её. Если они совпадают, запрос подлинный.
Выбор правильного алгоритма
HMAC-SHA256 является де-факто стандартом. Он быстрый, безопасный и доступен во всех основных языках. Избегайте MD5 или SHA1, которые считаются слабыми.
Пошагово: проверка подписанных данных
- Извлеките подпись из заголовка (например,
X-Signature). - Получите исходное тело запроса в виде строки или массива байтов. Не разбирайте его заранее; любое преобразование изменит подпись.
- Вычислите HMAC с помощью вашего секрета и исходного тела.
- Сравните вычисленную подпись с полученной, используя сравнение с постоянным временем, чтобы предотвратить атаки по времени.
Вот пример на Node.js с использованием Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body parser for webhook route
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(req.body);
// ... handle event
res.status(200).send('OK');
});
Распространённая ошибка: использование парсера JSON-тела до проверки. express.json() в Express разбирает тело, поэтому исходные байты теряются. Всегда используйте express.raw() для маршрутов вебхуков или сохраняйте исходное тело отдельно.
Разработка стратегии повторных попыток
Даже с подписями вебхуки могут завершаться неудачей: ваш сервер может быть недоступен или запрос может истечь по таймауту. Надёжный отправитель повторяет неудачные доставки. Как получатель, вы также должны корректно обрабатывать повторные попытки.
Повторные попытки на стороне отправителя
Если вы создаёте отправителя, реализуйте экспоненциальную задержку с джиттером. Например, повторяйте через 1 минуту, затем через 5 минут, затем через 30 минут, до максимального числа попыток (например, 5). Включите уникальный ID события в данные, чтобы получатель мог выполнить дедупликацию.
Идемпотентность на стороне получателя
Поскольку повторные попытки могут привести к дублирующимся доставкам, ваша конечная точка должна быть идемпотентной. Используйте ID события для отслеживания обработанных событий. Сохраняйте его в базе данных с уникальным ограничением или используйте кэш, например Redis, с TTL. Если ID события уже встречался, немедленно верните 200 OK без повторной обработки.
Пример с использованием уникального индекса базы данных:
async function handleWebhook(event) {
try {
await db.query('INSERT INTO processed_events (event_id) VALUES ($1)', [event.id]);
} catch (err) {
if (err.code === '23505') { // unique violation
return; // already processed
}
throw err;
}
// process event
}
Лучшие практики для конечных точек вебхуков
- Отвечайте быстро: возвращайте статус 2xx в течение нескольких секунд. Перекладывайте тяжёлую обработку на фоновую задачу.
- Проверяйте метку времени: включайте метку времени в подпись, чтобы предотвратить атаки повторного воспроизведения. Отклоняйте запросы старше нескольких минут.
- Используйте HTTPS: всегда используйте TLS для защиты данных при передаче.
- Логируйте всё: записывайте входящие вебхуки, включая заголовки и тело, для отладки.
- Отслеживайте сбои: настройте оповещения о повторяющихся сбоях или несовпадениях подписей.
Сравнение: подписанные и неподписанные вебхуки
| Аспект | Подписанные вебхуки | Неподписанные вебхуки |
|---|---|---|
| Безопасность | Высокая — проверяет отправителя и целостность | Низкая — любой может подделать запросы |
| Сложность реализации | Средняя — требует HMAC и управления секретами | Низкая — просто принимайте POST |
| Варианты использования | Платежи, конфиденциальные данные | Некритичные уведомления |
Часто задаваемые вопросы
Как проверить подпись вебхука в Python?
Используйте модуль hmac с SHA256. Сравните вычисленный дайджест с заголовком с помощью hmac.compare_digest для сравнения с постоянным временем.
Что делать, если моя конечная точка вебхука получает дублирующееся событие?
Проверьте ID события по хранилищу обработанных ID. Если это дубликат, верните 200 OK и пропустите обработку. Это обеспечивает идемпотентность.
Сколько повторных попыток должен выполнять отправитель вебхука?
Фиксированного числа нет, но обычно делают 3–5 повторных попыток с экспоненциальной задержкой (например, 1 мин, 5 мин, 30 мин). После этого отметьте доставку как неудачную и отправьте оповещение.
Готовы протестировать свою конечную точку вебхука? Используйте наш JSON Formatter для форматирования и проверки данных во время разработки.