Вебхуки: подписанные данные и повторные попытки
Почему вебхуки — это сложно
Вебхуки — простая идея: вместо опроса API на предмет изменений провайдер отправляет события на ваш эндпоинт через HTTP POST. Но на практике вебхуки создают ряд проблем, с которыми сталкиваются многие разработчики: проверка того, что запрос действительно пришёл от провайдера, обработка дублирующихся доставок и сбоев сети. Если игнорировать эти аспекты, вы рискуете безопасностью, целостностью данных и лояльностью пользователей.
Эта статья рассматривает два наиболее критичных аспекта production-вебхуков: подписанные данные и повторные попытки. Мы расскажем, как проверять подписи, как реализовать повторные попытки с экспоненциальной задержкой и как сделать обработку идемпотентной.
Подписанные данные: подтверждение подлинности
Любой может отправить HTTP POST на ваш эндпоинт. Чтобы убедиться, что запрос пришёл от вашего провайдера, провайдер подписывает данные секретным ключом и отправляет подпись в заголовке. Ваша задача — пересчитать подпись и сравнить её.
Большинство провайдеров используют HMAC-SHA256. Подпись обычно вычисляется по необработанному телу запроса (иногда с временной меткой) и кодируется в hex. Например, Stripe использует заголовок вида Stripe-Signature, содержащий временную метку и подпись.
Как работает HMAC
HMAC (код аутентификации сообщений на основе хэша) объединяет секретный ключ с хэш-функцией. При одинаковых ключе и сообщении результат детерминирован. Без ключа злоумышленник не может подделать действительную подпись.
Вот минимальный пример на Node.js:
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)
);
}
На Python это столь же просто:
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Лучшие практики проверки подписи
- Используйте сравнение с постоянным временем, чтобы предотвратить атаки по времени. Оба метода —
crypto.timingSafeEqualиhmac.compare_digest— безопасны. - Проверяйте необработанное тело, а не распарсенный JSON-объект. Парсинг и повторная сериализация могут изменить пробелы и нарушить подпись.
- Включайте временную метку в подписываемые данные, чтобы предотвратить атаки повторного воспроизведения. Отклоняйте запросы старше нескольких минут.
- Храните секреты безопасно — используйте переменные окружения или менеджер секретов, никогда не встраивайте их в код.
Обработка временных меток и атак повторного воспроизведения
Многие провайдеры включают временную метку в заголовок подписи. Например, заголовок может выглядеть так: t=1614556800,v1=abc123.... Вам следует:
- Извлечь временную метку и подпись.
- Проверить, что временная метка находится в допустимом окне (например, 5 минут).
- Пересчитать подпись, используя временную метку и тело запроса.
- Безопасно сравнить подписи.
Это предотвращает повторное воспроизведение старого действительного запроса злоумышленником.
Повторные попытки: изящная обработка сбоев
Вебхуки доставляются через публичный интернет, поэтому сбои неизбежны. Ваш эндпоинт может быть недоступен, работать медленно или возвращать ошибку 500. Надёжный провайдер будет повторять неудачные доставки, но если вы сами являетесь провайдером, вам также нужно обрабатывать повторные попытки на своей стороне.
Экспоненциальная задержка с джиттером
При повторных попытках избегайте немедленных повторов, которые перегружают эндпоинт. Используйте экспоненциальную задержку: увеличивайте время ожидания после каждой неудачи. Добавьте джиттер (случайность), чтобы избежать проблемы «стада» (thundering herd), когда множество вебхуков терпят неудачу одновременно.
Типичное расписание повторных попыток может выглядеть так:
| Попытка | Задержка (прибл.) |
|---|---|
| 1 | Немедленно |
| 2 | 30 секунд |
| 3 | 5 минут |
| 4 | 30 минут |
| 5 | 2 часа |
После максимального числа попыток пометьте доставку как неудачную и отправьте уведомление.
Идемпотентность: ключ к безопасным повторным попыткам
Поскольку повторные попытки могут вызывать дублирующиеся доставки, ваш обработчик вебхуков должен быть идемпотентным. Это означает, что обработка одного и того же события несколько раз должна иметь тот же эффект, что и однократная обработка.
Как достичь идемпотентности:
- Используйте уникальный ID события, предоставленный в теле запроса (например, поле
id). Сохраняйте обработанные ID в базе данных с уникальным ограничением. - Проектируйте операции идемпотентными — например, используйте upsert вместо insert или проверяйте существование ресурса перед созданием.
- Быстро возвращайте 2xx и обрабатывайте асинхронно, если работа тяжёлая. Это снижает вероятность таймаутов, вызывающих повторные попытки.
Реализация повторных попыток на стороне провайдера
Если вы создаёте систему вебхуков, вам нужен надёжный механизм доставки. Вот упрощённый подход:
- При возникновении события поставьте в очередь задание на доставку с телом запроса и целевым URL.
- Воркер берёт задание и отправляет HTTP POST с таймаутом (например, 10 секунд).
- Если статус ответа не 2xx, запланируйте повторную попытку с экспоненциальной задержкой.
- Отслеживайте попытки и прекращайте после максимума (например, 5 попыток).
- Логируйте сбои и предоставьте пользователям возможность ручного повтора.
Используйте постоянную очередь (например, Redis, RabbitMQ или таблицу в базе данных), чтобы повторные попытки переживали перезапуски.
Собираем всё вместе: чек-лист получателя вебхуков
При создании получателя вебхуков следуйте этому чек-листу:
- Проверьте подпись, используя секрет провайдера и сравнение с постоянным временем.
- Проверьте временную метку, чтобы предотвратить атаки повторного воспроизведения.
- Быстро ответьте 2xx — подтвердите получение до начала тяжёлой обработки.
- Обрабатывайте асинхронно при необходимости, используя очередь.
- Обеспечьте идемпотентность, отслеживая ID событий.
- Логируйте всё для отладки и аудита.
- Мониторьте сбои и настройте оповещения при повторных попытках.
FAQ
Что такое подписанные данные вебхука?
Подписанные данные включают криптографическую подпись (обычно HMAC-SHA256), сгенерированную с общим секретом. Получатель пересчитывает подпись, чтобы убедиться, что запрос пришёл от ожидаемого отправителя и не был изменён.
Зачем вебхукам повторные попытки?
Проблемы с сетью, простои сервера или временные ошибки могут привести к сбою доставки вебхука. Повторные попытки с экспоненциальной задержкой гарантируют, что событие в конечном итоге будет доставлено, не перегружая получателя.
Как предотвратить дублирующую обработку вебхуков?
Используйте идемпотентность: включайте уникальный ID события в тело запроса, сохраняйте обработанные ID и проектируйте операции идемпотентными (например, upsert). Таким образом, даже если вебхук доставлен несколько раз, эффект будет одинаковым.
Если вам нужно просмотреть или отладить данные вебхуков, JSON Formatter поможет быстро отформатировать и проверить JSON-данные.