Вебхуки: подписанные данные и повторные попытки
Вы создали API и теперь нужно уведомлять другие сервисы о происходящих событиях. Вы настроили конечную точку вебхука, но вскоре обнаружили, что любой может отправлять поддельные данные, а когда ваш сервер недоступен, события теряются навсегда. Это распространенная ошибка: вебхуки просты в концепции, но требуют тщательного проектирования для обеспечения безопасности и надежности.
В этом руководстве мы рассмотрим основы вебхуков: как подписывать данные для предотвращения подделки, как проверять подписи на принимающей стороне и как реализовать логику повторных попыток с экспоненциальной задержкой для обработки ошибок. Мы также рассмотрим идемпотентность, чтобы избежать дублирования обработки. К концу у вас будет надежная система вебхуков, которой можно доверять.
Что такое вебхуки?
Вебхуки — это определяемые пользователем HTTP-обратные вызовы. Когда в исходной системе происходит событие (например, обрабатывается платеж), она отправляет HTTP POST-запрос на указанный вами URL с данными, содержащими детали события. Это позволяет осуществлять связь между сервисами в реальном времени без опроса.
Однако вебхуки создают две проблемы:
- Безопасность: Как получатель узнает, что запрос действительно пришел от отправителя?
- Надежность: Что делать, если получатель временно недоступен? Как гарантировать, что событие будет доставлено в конечном итоге?
Защита вебхуков с помощью подписанных данных
Чтобы предотвратить подделку запросов, отправитель должен подписать данные с помощью общего секрета. Получатель затем может проверить подпись, чтобы аутентифицировать отправителя. Наиболее распространенный метод — HMAC (код аутентификации сообщения на основе хэша) с SHA-256.
Как работает подпись HMAC
- Отправитель и получатель используют общий секретный ключ (желательно длинную случайную строку).
- Для каждого вебхука отправитель вычисляет HMAC тела запроса с использованием секрета и включает его в заголовок (например,
X-Signature). - Получатель вычисляет тот же HMAC для полученного тела и сравнивает его со значением заголовка. Если они совпадают, запрос подлинный.
Крайне важно подписывать исходное тело запроса, а не его разобранную версию, чтобы избежать расхождений из-за различий в сериализации JSON.
Пример: подпись данных в Node.js
const crypto = require('crypto');
function signPayload(payload, secret) {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
// На стороне отправителя:
const payload = JSON.stringify({ event: 'payment.success', amount: 100 });
const signature = signPayload(payload, 'your-secret-key');
// Включите подпись в заголовок: X-Signature: signature
Проверка подписи на получателе
На принимающей стороне вы должны проверить подпись перед обработкой данных. Используйте сравнение с постоянным временем, чтобы предотвратить атаки по времени.
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// В вашей конечной точке вебхука:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.rawBody; // исходное тело в виде строки
if (!verifySignature(payload, signature, 'your-secret-key')) {
return res.status(401).send('Invalid signature');
}
// Обработайте событие
res.status(200).send('OK');
});
Примечание: Всегда используйте HTTPS для предотвращения атак типа «человек посередине». Также рассмотрите возможность включения временной метки в подписанные данные для предотвращения повторных атак.
Реализация повторных попыток с экспоненциальной задержкой
Даже при надежном получателе сетевые проблемы или временные сбои могут привести к сбою доставки вебхука. Надежный отправитель должен повторять неудачные доставки. Однако наивные повторные попытки могут перегрузить получателя или вызвать дублирование обработки. Решение: экспоненциальная задержка с джиттером и максимальным лимитом повторных попыток.
Стратегия повторных попыток
- Повтор при сбое: Повторяйте, когда получатель возвращает код состояния, отличный от 2xx, или происходит тайм-аут.
- Экспоненциальная задержка: Увеличивайте задержку между повторными попытками экспоненциально (например, 1 с, 2 с, 4 с, 8 с...).
- Джиттер: Добавьте случайное изменение к задержке, чтобы избежать проблемы «стада».
- Максимальное количество попыток: Установите лимит (например, 5 повторных попыток), после чего пометьте вебхук как неудавшийся (возможно, уведомив администратора).
Пример: логика повторных попыток в Node.js
async function deliverWebhook(url, payload, secret, maxRetries = 5) {
const signature = signPayload(payload, secret);
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
body: payload,
});
if (response.ok) return;
throw new Error(`HTTP ${response.status}`);
} catch (error) {
attempt++;
if (attempt >= maxRetries) throw error;
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Эта функция повторяет попытки до 5 раз с экспоненциальной задержкой и джиттером, ограничивая задержку 30 секундами. Настройте параметры в соответствии с вашими потребностями.
Обеспечение идемпотентности
Повторные попытки могут вызвать дублирование доставок, поэтому ваш обработчик вебхуков должен быть идемпотентным. Включите уникальный идентификатор события в данные и обеспечьте отслеживание обработанных идентификаторов на получателе, чтобы избежать двойной обработки.
// На получателе:
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const { id } = req.body;
if (processedEvents.has(id)) {
return res.status(200).send('Already processed');
}
// Обработайте событие
processedEvents.add(id);
res.status(200).send('OK');
});
В производственной среде используйте постоянное хранилище (например, Redis или базу данных) для отслеживания обработанных идентификаторов между перезапусками.
Сравнение: подписанные и неподписанные вебхуки
| Аспект | Неподписанные | Подписанные (HMAC) |
|---|---|---|
| Безопасность | Любой может подделать запросы | Только владелец секрета может создать действительную подпись |
| Целостность | Данные могут быть изменены | Изменение делает подпись недействительной |
| Сложность | Низкая | Средняя (управление ключами, проверка) |
| Вариант использования | Внутренние доверенные сети | Любой публичный вебхук |
Часто задаваемые вопросы
Что такое подпись вебхука?
Подпись вебхука — это криптографический хэш (обычно HMAC) данных, созданный с использованием общего секрета. Она позволяет получателю убедиться, что запрос пришел от ожидаемого отправителя и что данные не были изменены.
Сколько раз следует повторять неудачный вебхук?
Нет фиксированного числа, но обычно практикуется 3-5 повторных попыток с экспоненциальной задержкой. После этого пометьте доставку как неудачную и уведомите вашу команду. Учитывайте критичность события: уведомления о платежах могут требовать большего числа повторных попыток, чем некритичные обновления.
Можно ли использовать вебхуки без HTTPS?
Никогда не используйте вебхуки по простому HTTP в производственной среде. HTTPS шифрует данные при передаче, предотвращая подслушивание и атаки типа «человек посередине». Даже с подписями HTTP раскрывает конфиденциальные данные.
Готовы протестировать ваши данные вебхуков? Используйте наш JSON Formatter для форматирования и проверки JSON-данных перед отправкой или отладкой.