Вебхуки: подписанные данные и повторные попытки
Вы создали конечную точку вебхука для получения событий от стороннего сервиса. Всё работает при тестировании, но в продакшене вы замечаете дублирующиеся события, пропущенные уведомления и иногда возникают опасения по поводу безопасности поддельных запросов. Это распространённые ловушки при реализации вебхуков без надлежащих мер защиты.
Вебхуки — это HTTP-колбэки, которые позволяют сервисам уведомлять ваше приложение о произошедших событиях. В отличие от опроса (polling), они обеспечивают обновления в реальном времени. Но чтобы использовать их надёжно, необходимо решить два критических аспекта: подлинность (убедиться, что запрос пришёл от ожидаемого отправителя) и доставка (обработка сбоев и повторных попыток). В этой статье объясняются оба аспекта с практическими примерами кода.
Как работают вебхуки
Вебхук — это определяемая пользователем конечная точка HTTP POST. Когда происходит событие, исходный сервис отправляет запрос на этот URL с данными, описывающими событие. Например, платёжный процессор может отправить вебхук при успешном списании средств.
Процесс прост:
- Вы регистрируете URL вебхука в сервисе.
- Сервис отправляет HTTP POST на этот URL при возникновении события.
- Ваша конечная точка обрабатывает данные и возвращает статус 2xx для подтверждения получения.
Без надлежащей проверки любой может отправлять запросы на вашу конечную точку. Без обработки повторных попыток временные сбои могут привести к потере событий. Давайте разберёмся с каждым аспектом.
Защита вебхуков с помощью подписанных данных
Чтобы убедиться, что вебхук пришёл от ожидаемого отправителя, сервисы подписывают данные с использованием общего секрета. Подпись обычно представляет собой HMAC (код аутентификации сообщения на основе хеша) от тела запроса, отправляемый в заголовке, например X-Signature.
Как работает подпись HMAC
Отправитель вычисляет HMAC с использованием секретного ключа и необработанного тела запроса, затем включает шестнадцатеричную подпись в заголовок. Ваша конечная точка пересчитывает HMAC с тем же секретом и сравнивает его с полученной подписью. Если они совпадают, запрос подлинный.
Вот минимальный пример на Node.js с использованием Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Используйте необработанное тело для проверки подписи
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body;
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');
}
// Обработка события
const event = JSON.parse(payload);
// ... обработка события
res.status(200).send('OK');
});
Ключевые моменты:
- Всегда используйте необработанное тело запроса для проверки, а не разобранный объект. Парсинг может изменить данные (например, пробелы) и нарушить подпись.
- Используйте сравнение, устойчивое к атакам по времени, чтобы предотвратить timing-атаки.
- Храните секреты в безопасности (переменные окружения, менеджеры секретов) и периодически меняйте их.
Дополнительные меры безопасности
- Проверка временной метки: Включите временную метку в подпись (например,
t=timestamp,v1=signature) и отклоняйте запросы старше нескольких минут, чтобы предотвратить атаки повторного воспроизведения. - Белый список IP-адресов: Если провайдер публикует диапазоны IP, ограничьте вашу конечную точку этими IP-адресами.
- HTTPS: Всегда используйте HTTPS для защиты данных при передаче.
Реализация надёжных повторных попыток
Доставка вебхуков не гарантируется. Сети могут отказывать, ваш сервер может быть недоступен, или вы можете вернуть статус, отличный от 2xx. Чтобы справиться с этим, отправители обычно повторяют попытки с экспоненциальной задержкой. Как получатель, вы должны спроектировать свою конечную точку идемпотентной и быстро отвечать.
На стороне отправителя (если вы отправляете вебхуки)
Если вы создаёте сервис, отправляющий вебхуки, реализуйте повторные попытки с задержкой. Распространённая стратегия:
- Повторять при сетевых ошибках или ответах 5xx.
- Не повторять при ответах 4xx (кроме 429 Too Many Requests).
- Использовать экспоненциальную задержку с джиттером: ждать 1 с, 2 с, 4 с, 8 с и т.д. до максимума.
- Установить максимальное количество повторных попыток (например, 5) и рассмотреть очередь недоставленных сообщений для сбоев.
Пример логики повторных попыток на Python:
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if 200 <= response.status_code < 300:
return True
elif 400 <= response.status_code < 500 and response.status_code != 429:
# Ошибка клиента, не повторять
return False
except RequestException:
pass
# Экспоненциальная задержка с джиттером
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
На стороне получателя
Ваша конечная точка должна:
- Быстро подтверждать: Возвращать 2xx в течение нескольких секунд. Если обработка занимает больше времени, поставьте событие в очередь и обработайте асинхронно.
- Быть идемпотентной: Используйте ID события из данных для дедупликации. Сохраняйте обработанные ID и игнорируйте повторы.
- Обрабатывать конкурентность: Используйте транзакции базы данных или блокировки, чтобы избежать состояний гонки при одновременной обработке одного и того же события.
Пример идемпотентности с использованием уникального ограничения базы данных:
CREATE TABLE processed_events (
event_id VARCHAR(255) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Перед обработкой попытайтесь вставить ID события. Если возникает нарушение первичного ключа, пропустите обработку.
Тестирование вебхуков локально
Во время разработки вам нужен публичный URL, чтобы сервис мог достичь вашего локального сервера. Инструменты вроде ngrok или localtunnel предоставляют доступ к localhost. Всегда используйте тестовые секреты и проверяйте подписи в процессе разработки, чтобы выявлять проблемы на ранних этапах.
Сравнение: опрос vs вебхуки
| Аспект | Опрос | Вебхуки |
|---|---|---|
| Задержка | Зависит от интервала | Почти в реальном времени |
| Нагрузка на сервер | Высокая (частые запросы) | Низкая (только при событиях) |
| Сложность | Простота реализации | Требуется проверка подписи и обработка повторных попыток |
| Надёжность | Вы контролируете повторные попытки | Зависит от политики повторных попыток отправителя |
Часто задаваемые вопросы
Что такое подписанные данные вебхука?
Подписанные данные включают криптографическую подпись (обычно HMAC), сгенерированную с использованием общего секрета. Это позволяет получателю убедиться, что запрос пришёл от ожидаемого отправителя и не был изменён.
Как обрабатывать дублирующиеся доставки вебхуков?
Сделайте вашу конечную точку идемпотентной, используя уникальный ID события из данных. Сохраняйте обработанные ID и игнорируйте дубликаты. Это гарантирует, что повторные попытки или сетевые проблемы не приведут к двойной обработке.
Следует ли повторять неудачные доставки вебхуков?
Да, но только для временных ошибок (сетевые проблемы, 5xx). Используйте экспоненциальную задержку с джиттером и установите максимальное количество повторных попыток. Избегайте повторных попыток при ошибках 4xx (кроме 429), так как они указывают на проблему с самим запросом.
При отладке данных вебхуков часто требуется просмотреть JSON-данные. JSON Formatter поможет вам быстро отформатировать и проверить JSON-данные.