Вебхуки: подписанные payload и повторные попытки
Вы только что интегрировали вебхук платежного провайдера. В документации сказано: «мы отправим POST на ваш эндпоинт, когда платеж пройдет успешно». Просто, правда? А потом вы замечаете дублирующиеся события, пропущенные уведомления и предупреждение о безопасности насчет поддельных запросов. Вебхуки просты в концепции, но коварны в продакшене. В этой статье объясняется, как правильно реализовать вебхуки с использованием подписанных payload и повторных попыток, чтобы вы могли строить надежные и безопасные интеграции.
Что такое вебхуки?
Вебхук — это обратный API: вместо того чтобы ваше приложение опрашивало сервис на предмет обновлений, сервис отправляет события на ваш эндпоинт через HTTP POST. Это эффективно и почти в реальном времени. Типичные применения включают уведомления о платежах, триггеры CI/CD и интеграции с чатами.
Но вебхуки создают проблемы: как узнать, что запрос действительно пришел от провайдера? Что если ваш сервер недоступен? Что если одно и то же событие приходит дважды? Давайте разберемся с этим по порядку.
Защита вебхуков с помощью подписанных payload
Любой может отправить HTTP-запрос на ваш эндпоинт. Без проверки злоумышленники могут подделать события, что приведет к мошенничеству или повреждению данных. Стандартное решение — подписывать payload общим секретом.
Как работают HMAC-подписи
Провайдер и вы используете общий секретный ключ. Для каждого события провайдер вычисляет HMAC (код аутентификации сообщения на основе хеша) тела запроса с использованием этого секрета и отправляет подпись в заголовке (например, X-Signature). Вы пересчитываете HMAC на своей стороне и сравниваете. Если они совпадают, запрос подлинный и не изменен.
Вот минимальный пример на Python с использованием Flask:
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_shared_secret'
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not signature:
abort(400, 'Missing signature')
body = request.get_data()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
# Process the event
return '', 200
Всегда используйте hmac.compare_digest для сравнения за постоянное время, чтобы предотвратить timing-атаки. Кроме того, проверяйте подпись до разбора JSON-тела, чтобы избежать ненужной обработки.
Лучшие практики для подписей
- Используйте надежный секрет: сгенерируйте случайный ключ длиной не менее 32 байт и храните его в безопасном месте (например, в переменной окружения).
- Включайте временную метку: для предотвращения replay-атак провайдеры часто включают временную метку в подписанный payload. Отклоняйте запросы старше нескольких минут.
- Периодически ротируйте секреты: поддерживайте несколько активных секретов во время ротации, чтобы избежать простоя.
- Документируйте алгоритм подписи: укажите хеш-функцию (стандарт — SHA-256) и имя заголовка.
Обработка сбоев с помощью повторных попыток
С точки зрения провайдера вебхуки — это «выстрелил и забыл», но ваш эндпоинт может быть временно недоступен. Чтобы обеспечить доставку, провайдеры реализуют политики повторных попыток. Как получатель, вы должны быть готовы к повторам и дубликатам.
Стратегии повторов, используемые провайдерами
Большинство провайдеров повторяют с экспоненциальной задержкой: например, сразу, затем через 1 минуту, 5 минут, 30 минут и т.д., вплоть до максимального числа попыток (часто 5–10). Некоторые также позволяют настроить расписание повторов. Точное поведение различается, поэтому читайте документацию провайдера.
Проектирование эндпоинта для повторных попыток
Ваш эндпоинт должен:
- Отвечать быстро: подтверждать получение статусом 2xx в течение нескольких секунд. Выполняйте тяжелую обработку асинхронно (например, ставьте событие в очередь).
- Быть идемпотентным: одно и то же событие может быть доставлено несколько раз. Используйте ключ идемпотентности (часто предоставляется в payload события или заголовке) для дедупликации. Храните ID обработанных событий в течение разумного периода.
- Возвращать соответствующие коды состояния: 2xx при успехе, 4xx при клиентских ошибках (провайдер может не повторять), 5xx при серверных ошибках (провайдер повторит).
Пример проверки идемпотентности на Python:
processed_events = set() # In production, use a database or cache
@app.route('/webhook', methods=['POST'])
def webhook():
# ... signature verification ...
event = request.json
event_id = event['id']
if event_id in processed_events:
return '', 200 # Already processed
# Process event
processed_events.add(event_id)
return '', 200
Тестирование реализации вебхуков
Перед запуском в продакшен тщательно протестируйте ваш эндпоинт:
- Симулируйте подписанные запросы: напишите модульные тесты, которые генерируют валидные и невалидные подписи.
- Тестируйте сценарии повторов: симулируйте сбои и убедитесь, что ваш эндпоинт обрабатывает дубликаты.
- Используйте инструменты провайдера: многие провайдеры предлагают CLI или панель управления для отправки тестовых событий.
- Мониторинг и логирование: логируйте входящие события (без конфиденциальных данных) и настройте оповещения о сбоях подписи или высоком уровне ошибок.
Сравнение: опрос vs вебхуки
| Аспект | Опрос | Вебхуки |
|---|---|---|
| Задержка | Зависит от интервала (например, минуты) | Почти в реальном времени |
| Нагрузка на сервер | Высокая (частые запросы) | Низкая (только по событиям) |
| Сложность | Просто реализовать | Требуется проверка подписи и обработка повторов |
| Надежность | Вы контролируете повторы | Провайдер контролирует повторы; вы должны обрабатывать дубликаты |
FAQ
Как проверить подпись вебхука?
Вычислите HMAC сырого тела запроса с использованием общего секрета и сравните его с заголовком подписи с помощью функции сравнения за постоянное время. Всегда проверяйте перед обработкой payload.
Что должен возвращать мой эндпоинт для подтверждения вебхука?
Возвращайте код состояния 2xx (например, 200 OK) как только вы получили и поставили событие в очередь на обработку. Избегайте длительной обработки в обработчике запроса, чтобы предотвратить тайм-ауты.
Как обрабатывать дублирующиеся доставки вебхуков?
Используйте ключ идемпотентности из payload события (или уникальный ID события) для отслеживания обработанных событий. Храните эти ID в базе данных или кэше и пропускайте обработку, если ID уже встречался ранее.
Готовы протестировать ваши payload вебхуков? Используйте наш JSON Formatter для форматирования и валидации JSON-тела входящих вебхуков, что упрощает отладку.