Вебхуки: подписанные payload и логика повторных попыток
Вы создали API и теперь нужно уведомлять другие сервисы о происходящих событиях. Вы решаете реализовать вебхуки: HTTP-колбэки, которые отправляют данные на URL подписчиков. Но вскоре возникают два критических вопроса: как убедиться, что payload не был подменён? И что произойдёт, если принимающий сервер недоступен или возвращает ошибку? Без надлежащих мер защиты вебхуки могут стать дырой в безопасности и источником проблем с надёжностью.
В этом руководстве мы разберём основы защиты вебхуков с помощью подписанных payload и реализуем надёжную логику повторных попыток. Вы узнаете, как генерировать и проверять HMAC-подписи, проектировать идемпотентные обработчики и создавать механизм повторных попыток, который корректно обрабатывает сбои. К концу у вас будет готовый к продакшену подход к вебхукам.
Почему вебхукам нужны безопасность и надёжность
Вебхуки — это, по сути, HTTP-запросы между серверами, инициируемые событиями. В отличие от вызовов API, инициируемых клиентом, вебхуки отправляются с вашего сервера на эндпоинт подписчика. Эта модель push создаёт две основные проблемы:
- Безопасность: Payload передаётся через интернет и может быть перехвачен или подделан. Подписчики должны проверять, что запрос действительно пришёл от вашего сервиса.
- Надёжность: Сервер подписчика может быть временно недоступен, медленно отвечать или возвращать ошибку. Нужна стратегия повторных попыток доставки без дублирования обработки.
Игнорирование этих проблем может привести к утечкам данных, потере событий или несогласованности состояния между системами. Давайте разберём их по порядку.
Защита вебхуков с помощью подписанных payload
Подписанный payload использует криптографический хеш для подтверждения целостности и подлинности запроса. Наиболее распространённый подход — HMAC (Hash-based Message Authentication Code) с общим секретом. Вот как это работает:
- Общий секрет: Отправитель (ваш сервис) и получатель (подписчик) совместно используют секретный ключ. Этот ключ генерируется при регистрации подписчиком своего эндпоинта вебхука.
- Генерация подписи: Для каждого запроса вебхука отправитель вычисляет HMAC тела запроса (а иногда и заголовков, например, временной метки) с использованием секрета. Подпись отправляется в заголовке, например,
X-Signature. - Проверка подписи: Получатель пересчитывает HMAC с тем же секретом и сравнивает с полученной подписью. Если они совпадают, payload аутентичен и не изменён.
Рассмотрим пример кода на Node.js для генерации и проверки подписей.
const crypto = require('crypto');
// Отправитель: генерация подписи
function generateSignature(payload, secret) {
return crypto.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
}
// Получатель: проверка подписи
function verifySignature(payload, secret, receivedSignature) {
const expectedSignature = generateSignature(payload, secret);
// Используйте timingSafeEqual для предотвращения атак по времени
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
Важно: Всегда используйте функцию сравнения с постоянным временем, такую как timingSafeEqual, чтобы избежать атак по времени. Также включайте временную метку в подписываемые данные для предотвращения повторных атак. Например, подписывайте timestamp + '.' + payload и отклоняйте запросы старше нескольких минут.
Лучшие практики для подписанных payload
- Используйте HMAC-SHA256 или более стойкий алгоритм.
- Включайте временную метку в подпись и устанавливайте максимальный возраст (например, 5 минут).
- Никогда не передавайте секрет в запросе; он используется только для подписи.
- Периодически меняйте секреты и поддерживайте несколько активных секретов во время ротации.
Реализация логики повторных попыток для надёжной доставки
Даже при наличии защиты вебхуки могут терпеть неудачу из-за сетевых проблем, ошибок сервера или таймаутов. Надёжный механизм повторных попыток гарантирует, что события в конечном итоге будут доставлены. Вот пошаговый подход:
- Установите таймаут: Определите разумный таймаут для запросов вебхука (например, 10 секунд). Если подписчик не отвечает вовремя, считайте это сбоем.
- Классифицируйте ответы: Коды состояния 2xx считайте успехом. Для ошибок 4xx (кроме 429) повторные попытки обычно бессмысленны, так как запрос неверен. Для ошибок 5xx и таймаутов повторяйте с экспоненциальной задержкой.
- Экспоненциальная задержка с джиттером: Планируйте повторные попытки с увеличивающимися интервалами (например, 1с, 5с, 30с, 5м, 30м) и добавляйте случайный джиттер, чтобы избежать эффекта «громового стада».
- Ограничьте количество попыток: Установите максимальное число попыток (например, 5), после чего помечайте доставку как неудачную. Опционально уведомляйте подписчика по электронной почте или через панель управления.
- Идемпотентность: Поскольку повторные попытки могут вызывать дублирование доставки, включайте уникальный ID события в payload и заголовок. Подписчики должны использовать этот ID для дедупликации обработки.
Вот упрощённая реализация очереди повторных попыток на Python с использованием requests и time:
import requests
import time
import random
def deliver_webhook(url, payload, secret, max_retries=5):
signature = generate_signature(payload, secret)
headers = {'X-Signature': signature, 'Content-Type': 'application/json'}
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
if 200 <= response.status_code < 300:
return True
elif response.status_code == 429 or 500 <= response.status_code < 600:
# Ошибка, которую можно повторить
pass
else:
# Ошибка, которую нельзя повторить
return False
except requests.exceptions.RequestException:
# Сетевая ошибка, повторяем
pass
# Экспоненциальная задержка с джиттером
delay = min(30, 2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
Идемпотентность: обработка дублирующихся доставок
Поскольку повторные попытки могут привести к многократной доставке одного и того же события, подписчики должны реализовать идемпотентную обработку. Стандартный подход — включить уникальный ID события (например, UUID) в payload, а получатель сохраняет обработанные ID на определённый период. Если получен дублирующийся ID, получатель подтверждает его без повторной обработки.
Вот простая проверка идемпотентности в обработчике вебхука:
processed_events = set()
def handle_webhook(event):
event_id = event['id']
if event_id in processed_events:
return 'already processed'
# Обработка события
processed_events.add(event_id)
return 'success'
В продакшене используйте постоянное хранилище, такое как Redis или база данных с TTL, для отслеживания обработанных ID.
Сравнение: подписанные payload против других мер безопасности
| Подход | Плюсы | Минусы |
|---|---|---|
| HMAC-подписанные payload | Надёжная целостность и подлинность; секреты не передаются | Требует управления общими секретами |
| Basic Auth | Простота реализации | Учётные данные передаются в каждом запросе; менее безопасно |
| IP-allowlisting | Блокирует неавторизованные источники | IP могут меняться; не подходит для динамических сред |
| OAuth2 client credentials | Стандартизировано; на основе токенов | Избыточно для простых вебхуков; накладные расходы на управление токенами |
HMAC-подписанные payload обеспечивают хороший баланс между безопасностью и простотой для большинства сценариев использования вебхуков.
Собираем всё вместе
Чтобы построить надёжную и безопасную систему вебхуков:
- Генерируйте уникальный секрет для каждого подписчика и подписывайте каждый payload с помощью HMAC.
- Включайте временную метку и ID события в подписываемые данные.
- Реализуйте повторные попытки с экспоненциальной задержкой и джиттером для временных сбоев.
- Обеспечьте идемпотентность на стороне получателя с использованием ID событий.
- Отслеживайте процент успешных доставок и оповещайте о сбоях.
При отладке доставки вебхуков неоценимо полезно инспектировать сырые HTTP-запросы и ответы. Если вы работаете с логами Nginx для отслеживания входящих запросов вебхуков, Nginx Log Analyzer поможет вам разбирать и фильтровать записи логов, чтобы выявить неудачные доставки или подозрительные шаблоны.
Часто задаваемые вопросы
Что такое подписанный payload вебхука?
Подписанный payload вебхука включает криптографическую подпись (обычно HMAC), которая позволяет получателю проверить подлинность и целостность запроса. Отправитель вычисляет подпись с использованием общего секрета и включает её в заголовок; получатель пересчитывает и сравнивает её.
Сколько раз следует повторять неудачный вебхук?
Распространённая практика — от 3 до 5 повторных попыток с экспоненциальной задержкой. После этого пометьте доставку как неудачную и уведомите подписчика. Точное число зависит от вашего SLA и критичности события.
Почему идемпотентность важна для вебхуков?
Повторные попытки могут вызывать дублирование доставки. Идемпотентность гарантирует, что многократная обработка одного и того же события имеет тот же эффект, что и однократная, предотвращая повреждение данных или дублирование действий.