Вебхуки: подписанные данные и повторные попытки

Backend2026-10-29TryQuickToolBox

Вы только что интегрировали стороннего платёжного провайдера. В документации сказано: «Мы отправим вебхук, когда платёж пройдёт успешно». Просто, правда? Но затем вы понимаете: как убедиться, что запрос действительно пришёл от них? Что если ваш сервер недоступен в момент отправки? Что если они отправят одно и то же событие дважды?

Вебхуки — мощный инструмент, но требуют аккуратной обработки. Это руководство охватывает основы: подпись данных для проверки подлинности, реализацию повторных попыток для обработки сбоев и обеспечение идемпотентности для избежания дублирующей обработки.

Что такое вебхуки?

Вебхук — это HTTP-колбэк: вместо того чтобы ваше приложение опрашивало API на предмет изменений, провайдер отправляет HTTP POST на указанный вами URL при возникновении события. Это делает интеграции эффективными и почти в реальном времени.

Типичные примеры включают уведомления о платежах (Stripe, PayPal), триггеры CI/CD (GitHub, GitLab) и платформы обмена сообщениями (Slack, Discord).

Зачем защищать вебхуки подписями?

Ваша конечная точка вебхука общедоступна. Любой может отправить на неё POST-запрос. Без проверки злоумышленник может подделать события — например, пометить неоплаченный заказ как оплаченный.

Подпись данных с использованием общего секрета позволяет убедиться, что запрос пришёл от ожидаемого отправителя и что данные не были изменены.

Как работают HMAC-подписи

Провайдер и вы используете общий секретный ключ. Для каждого вебхука провайдер вычисляет HMAC (код аутентификации сообщения на основе хэша) от данных, используя этот секрет, и включает подпись в заголовок (например, X-Signature). Вы пересчитываете HMAC на своей стороне и сравниваете.

HMAC обеспечивает целостность и подлинность, поскольку только тот, у кого есть секрет, может создать действительную подпись.

Реализация подписанных данных

Рассмотрим типичную реализацию на Node.js (Express) и Python (Flask). Принципы применимы к любому языку.

Шаг 1: Получение и хранение секрета

При регистрации вебхука провайдер выдаёт вам секрет. Храните его безопасно — в переменных окружения или менеджере секретов — никогда в коде.

Шаг 2: Захват исходного тела запроса

Для проверки подписи требуются точные исходные байты тела. Если ваш фреймворк парсит JSON до проверки, повторная сериализация может изменить байты и нарушить проверку.

В Express используйте express.raw({ type: 'application/json' }) для маршрута вебхука. Во Flask обращайтесь к request.get_data() до парсинга.

Шаг 3: Вычисление и сравнение подписей

Используйте функцию сравнения с постоянным временем для предотвращения атак по времени.

// Node.js (Express)
const crypto = require('crypto');

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature'];
  const secret = process.env.WEBHOOK_SECRET;
  const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
  
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process event
  const event = JSON.parse(req.body);
  // ... handle event
  res.status(200).send('OK');
});

В Python используйте hmac.compare_digest:

# Python (Flask)
import hmac, hashlib, os
from flask import request, abort

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Signature')
    secret = os.environ['WEBHOOK_SECRET'].encode()
    expected = hmac.new(secret, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(signature, expected):
        abort(401)
    event = request.get_json()
    # ... handle event
    return 'OK', 200

Шаг 4: Обработка временных меток (необязательно, но рекомендуется)

Некоторые провайдеры включают временную метку в подпись для предотвращения атак повторного воспроизведения. Например, схема Stripe подписывает timestamp.payload. Следует отклонять запросы с временными метками, слишком далёкими в прошлом (например, старше 5 минут).

Реализация повторных попыток

Сети дают сбои. Ваш сервер может быть недоступен, или запрос может истечь по таймауту. Надёжные системы вебхуков повторяют неудачные доставки.

На стороне провайдера

Если вы создаёте отправителя вебхуков, реализуйте повторные попытки с экспоненциальной задержкой. Распространённый шаблон: повтор через 1 минуту, затем через 5 минут, затем через 30 минут и т.д., до максимального числа попыток (например, 5). Включите уникальный ID события в данные, чтобы получатель мог выполнить дедупликацию.

На стороне получателя

Как получатель, вы должны:

Идемпотентность: обработка дублирующихся событий

Повторные попытки могут привести к дублирующимся событиям. Используйте ID события (включённый большинством провайдеров) для отслеживания обработанных событий. Перед обработкой проверьте, не встречался ли уже этот ID; если да, пропустите.

Храните обработанные ID в базе данных с TTL или используйте ключ идемпотентности в вашей бизнес-логике.

Тестирование вебхуков локально

Во время разработки ваш локальный сервер не доступен публично. Инструменты вроде ngrok или localtunnel создают публичный URL, который туннелируется на вашу машину. В качестве альтернативы используйте сервис тестирования вебхуков, который захватывает запросы и позволяет воспроизводить их.

Распространённые ошибки и лучшие практики

Сравнение: подписанные и неподписанные вебхуки

АспектПодписанныеНеподписанные
БезопасностьВысокая: проверяет источник и целостностьНизкая: любой может подделать
Трудозатраты на реализациюСредние: вычисление и сравнение HMACМинимальные
Рекомендуется дляВсех production-системТолько внутренних доверенных сетей

Часто задаваемые вопросы

Как проверить подпись вебхука?

Вычислите HMAC от исходного тела запроса, используя общий секрет, затем сравните его с заголовком подписи с помощью функции сравнения с постоянным временем. Всегда используйте исходное тело, а не распарсенный JSON.

Что должна возвращать моя конечная точка вебхука?

Быстро возвращайте код состояния 2xx, чтобы подтвердить получение. Если обработка не удалась, верните 5xx, чтобы вызвать повторную попытку, но убедитесь, что ваша конечная точка идемпотентна для обработки дубликатов.

Сколько раз будет повторяться вебхук?

Зависит от провайдера. Большинство повторяют несколько раз с экспоненциальной задержкой в течение нескольких часов или дней. Уточните в документации провайдера.

При отладке доставки вебхуков часто требуется просмотр серверных логов. Если вы используете Nginx, Nginx Log Analyzer поможет разобрать и проанализировать логи доступа, чтобы выявить неудачные запросы или необычные шаблоны.