Вебхуки: подписанные payload'ы и стратегии повторных попыток
Вы создали API, и теперь нужно уведомлять другие сервисы о происходящих событиях. Опрос (polling) неэффективен, поэтому вы реализуете вебхуки. Но вскоре сталкиваетесь с двумя распространёнными проблемами: как убедиться, что payload не был подменён, и что делать, если принимающий эндпоинт недоступен. Эта статья объясняет, как использовать подписанные payload'ы и стратегии повторных попыток, чтобы сделать ваши вебхуки безопасными и надёжными.
Зачем вебхукам нужны подписи и повторные попытки
Вебхуки — это HTTP-колбэки, инициируемые событиями. Их просто реализовать, но они создают проблемы безопасности и надёжности. Без подписей злоумышленник может подделать запросы к вашему эндпоинту, что приведёт к повреждению данных или несанкционированным действиям. Без повторных попыток временные сбои (например, перебои в сети или временные ошибки сервера) приводят к пропуску событий.
Подписанные payload'ы решают проблему подлинности: отправитель подписывает payload секретным ключом, а получатель проверяет подпись. Повторные попытки решают проблему надёжности: отправитель повторяет неудачные доставки с задержкой.
Как работают подписанные payload'ы
Подписанный payload включает криптографическую подпись, сгенерированную из тела запроса и общего секрета. Наиболее распространённый метод — HMAC (код аутентификации сообщений на основе хэша) с SHA-256.
Генерация подписи
Отправитель вычисляет HMAC исходного тела запроса с использованием секретного ключа. Подпись отправляется в заголовке, часто вместе с временной меткой для предотвращения атак повторного воспроизведения.
// Пример на Node.js: генерация HMAC-подписи
const crypto = require('crypto');
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return { timestamp, signature };
}
Включите временную метку и подпись в заголовки, например, X-Webhook-Timestamp и X-Webhook-Signature.
Проверка подписи
Получатель должен восстановить подписанный payload, используя полученные временную метку и исходное тело, затем вычислить HMAC и безопасно сравнить его.
// Пример на Node.js: проверка подписи
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Используйте сравнение, устойчивое к атакам по времени
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
Всегда используйте сравнение, устойчивое к атакам по времени, чтобы предотвратить timing-атаки. Также проверяйте, что временная метка является свежей (например, в пределах 5 минут), чтобы смягчить атаки повторного воспроизведения.
Проектирование стратегии повторных попыток
Повторные попытки необходимы для надёжной доставки вебхуков. Но наивные повторные попытки могут перегрузить получателя или вызвать дублирующую обработку. Хорошая стратегия повторных попыток включает:
- Экспоненциальная задержка: Увеличивайте интервал между попытками (например, 1с, 2с, 4с, 8с...).
- Максимальное число попыток: Ограничьте количество попыток (например, 5), чтобы избежать бесконечных циклов.
- Очередь недоставленных сообщений (dead-letter queue): После исчерпания попыток сохраните событие для ручной проверки.
- Идемпотентность: Включите уникальный ID события, чтобы получатель мог дедуплицировать.
Вот простой цикл повторных попыток в псевдокоде:
function deliverWithRetry(url, payload, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, { method: 'POST', body: payload });
if (response.ok) return;
} catch (error) {
// Логирование ошибки
}
attempt++;
const delay = Math.pow(2, attempt) * 1000; // экспоненциальная задержка
await new Promise(resolve => setTimeout(resolve, delay));
}
// Переместить в очередь недоставленных сообщений
}
Рассмотрите добавление джиттера к задержке, чтобы избежать проблемы «громового стада».
Сравнение: опрос vs вебхуки с повторными попытками
| Аспект | Опрос | Вебхуки с повторными попытками |
|---|---|---|
| Задержка | Высокая (зависит от интервала) | Низкая (почти в реальном времени) |
| Нагрузка на сервер | Постоянная (даже при отсутствии событий) | Зависит от событий |
| Надёжность | Высокая (контролируется получателем) | Зависит от стратегии повторных попыток |
| Безопасность | Стандартная аутентификация API | Требует подписанных payload'ов |
Пошагово: реализация вебхуков с подписями и повторными попытками
- Сгенерируйте секретный ключ для каждого эндпоинта вебхука и передайте его безопасным способом.
- При возникновении события создайте payload (JSON) и вычислите подпись с помощью HMAC-SHA256 с использованием секрета и временной метки.
- Отправьте POST-запрос на эндпоинт с заголовками:
Content-Type: application/json,X-Webhook-TimestampиX-Webhook-Signature. - На стороне получателя проверьте подпись, используя исходное тело и временную метку. Отклоните, если подпись неверна или временная метка слишком старая.
- Если доставка не удалась (ответ не 2xx или сетевая ошибка), повторите с экспоненциальной задержкой до максимального числа попыток.
- После исчерпания попыток залогируйте ошибку и, при необходимости, отправьте оповещение; сохраните событие в очередь недоставленных сообщений для ручного воспроизведения.
- Обеспечьте идемпотентность на получателе, используя ID события для игнорирования дубликатов.
Распространённые ошибки и лучшие практики
- Не использовать исходное тело: Многие фреймворки парсят JSON и изменяют тело. Используйте исходное тело для проверки подписи.
- Игнорирование временных меток: Без проверки временной метки возможны атаки повторного воспроизведения.
- Отсутствие идемпотентности: Повторные попытки могут вызвать дублирующую обработку. Используйте ключи идемпотентности.
- Бесконечные повторные попытки: Всегда устанавливайте максимальное число попыток и очередь недоставленных сообщений.
- Отсутствие логирования: Логируйте попытки доставки и ошибки для отладки.
Часто задаваемые вопросы
Что такое подписанный payload вебхука?
Подписанный payload вебхука включает криптографическую подпись (например, HMAC), которая позволяет получателю убедиться, что запрос пришёл от ожидаемого отправителя и не был подменён.
Сколько раз следует повторять неудачный вебхук?
Обычная практика — 3–5 повторных попыток с экспоненциальной задержкой. Точное число зависит от вашего SLA и критичности события. Всегда используйте очередь недоставленных сообщений для событий, превысивших лимит попыток.
Можно ли использовать вебхуки без подписей?
Это не рекомендуется. Без подписей ваш эндпоинт уязвим для поддельных запросов. Всегда используйте подписи (например, HMAC) и проверяйте временные метки для предотвращения атак повторного воспроизведения.
При отладке доставки вебхуков критически важно анализировать логи сервера. Если вы используете Nginx, Nginx Log Analyzer поможет вам разобрать и проанализировать логи доступа, чтобы отследить запросы вебхуков и выявить ошибки.