mTLS, JWS-подпись webhooks и AsyncAPI с outbox
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
Зачем третья глава
| Глава | Фокус |
|---|---|
| Проектирование API и интеграций | REST, контракт, партнёрский API заказов, HMAC-ошибки |
| Публичный API, OAuth 2.0 и webhooks | Публичный API, OAuth 2.0, webhooks с shared secret |
| Эта глава | Регулируемый B2B: mTLS на транспорте, JWS на теле webhook, AsyncAPI на поток событий, outbox — чтобы событие не потерялось после commit в БД |
Сценарий: банк-партнёр подключается к платформе выплат. Требования — взаимная аутентификация по сертификату, неотрекаемость доставки уведомлений, аудит по eventId, согласованный каталог сообщений до деплоя.
Слои доверия — что где проверять
- mTLS отвечает на вопрос "кто установил TCP/TLS-соединение" (сертификат клиента).
- OAuth / scope (если есть) — "какие операции разрешены этому client_id".
- JWS на webhook — "это тело не менялось после отправки платформой" (асимметричная подпись, ротация ключей через JWKS).
Подробнее про сертификаты: 8.07.112. HMAC из Публичный API, OAuth 2.0 и webhooks проще, но у крупных партнёров часто требуют асимметрию (платформа подписывает приватным ключом, банк проверяет публичным).
mTLS — проектирование доступа
Регистрация клиента
В консоли партнёра заводят запись:
| Поле | Пример | Зачем |
|---|---|---|
partnerId | bank_alfa | ключ в логах и rate limit |
clientCertSubjectCN | payments-api.bank.example | сопоставление с сертификатом |
allowedIPs | опционально | дополнительный фильтр |
environment | sandbox / prod | разные trust store |
Сертификат выпускает ваш CA или принимается сертификат из доверенного УЦ партнёра — политика фиксируется в договоре.
Терминация TLS
Типичная схема:
- API Gateway / Ingress завершает mTLS, прокидывает в заголовках
X-Client-Cert-Subjectили SPIFFE ID. - Сервис приложения не парсит PEM в каждом handler — идентичность уже проверена на краю.
Партнёр вызывает:
GET /v1/payouts/po_7f3c9a2e HTTP/1.1
Host: api.payments.example
# TLS: клиентский сертификат bank_alfa (clientAuth)
Accept: application/json
Ответ 403 без валидного клиентского сертификата — ожидаемое поведение; тело ошибки в том же формате, что в Проектирование API и интеграций.
Ротация сертификатов
- Перекрытие сроков: новый cert работает до отзыва старого (30–90 дней).
- В документации — fingerprint SHA-256 и дата
notAfter. - Алерт за 14 дней до истечения — обязанность обеих сторон.
mTLS дополняет OAuth для machine-to-machine: cert = "это именно инфраструктура банка", token = "этому client разрешён scope payouts:read".
Webhooks с JWS — вместо одного shared secret
Заголовки доставки
| Заголовок | Значение |
|---|---|
Content-Type | application/json |
X-Webhook-Id | evt_01JABC123 (идемпотентность — Методы и ключ идемпотентности) |
X-Webhook-Timestamp | 1716816005 |
X-Webhook-Signature | eyJhbGciOiJFERTQSIs... — compact JWS над каноническим payload |
Что подписываем
Строка для подписи (псевдоконтракт):
{timestamp}.{raw_body_bytes}
raw_body_bytes — тело HTTP до JSON.parse. Партнёр повторяет ту же конкатенацию и проверяет JWS алгоритмом из заголовка JWS (alg: EdDSA, RS256).
Пример тела:
{
"id": "evt_01JABC123",
"type": "payout.completed",
"createdAt": "2026-05-27T14:00:00Z",
"data": {
"payoutId": "po_7f3c9a2e",
"amount": { "value": "1500.00", "currency": "RUB" },
"status": "completed"
}
}
Платформа публикует JWKS по GET https://api.payments.example/.well-known/webhook-jwks.json — ротация kid без смены URL у партнёра.
Проверка на стороне банка (псевдокод)
Код ITЗагрузка примера кода…
Партнёр по-прежнему отвечает 2xx за секунды, тяжёлую обработку кладёт во внутреннюю очередь — как в Публичный API, OAuth 2.0 и webhooks.
AsyncAPI — контракт на события
OpenAPI описывает синхронные HTTP-вызовы. AsyncAPI — каналы, сообщения, схемы payload для Kafka/RabbitMQ и для исходящих webhooks как "канал доставки".
Фрагмент AsyncAPI 3:
Код ITЗагрузка примера кода…
Практика:
- одна JSON Schema на
PayoutCompleted— и в Kafka, и в webhook (нет расхождения полей); - ревью AsyncAPI до изменения producer;
- генерация типов (TypeScript, Java) и тестовых фикстур.
Синхронный REST по-прежнему в OpenAPI — 7.08.3.
Transactional outbox — от commit до webhook
Проблема: UPDATE payouts SET status='completed' закоммичен, а публикация в Kafka или HTTP webhook упала — партнёр не узнает о выплате.
Шаг 1 — одна транзакция в БД
Код ITЗагрузка примера кода…
Таблица outbox — источник правды до успешной доставки. Паттерн разобран в Saga / Outbox и событийной архитектуре.
Шаг 2 — relay в брокер
Worker читает published_at IS NULL, публикует в payments.payout.v1, помечает строку:
UPDATE outbox SET published_at = now() WHERE id = 'evt_01JABC123';
Несколько инстансов — FOR UPDATE SKIP LOCKED (пример в 5.03 Java / outbox).
Шаг 3 — delivery worker → webhook
Отдельный consumer (или тот же relay с веткой) строит конверт по AsyncAPI, подписывает JWS, шлёт POST на URL подписки банка. При 5xx — retry с backoff; при исчерпании — DLQ и алерт partnerId=bank_alfa.
Идемпотентность: банк хранит evt_01JABC123 72 часа; повторная доставка → 200 без повторного списания в их учёте.
Сводка — когда что включать
| Требование партнёра | Механизм |
|---|---|
| Доступ только с их ДЦ, без паролей в конфиге | mTLS |
| Scope и аудит по приложению | OAuth2 client credentials + mTLS |
| Неотрекаемость и ротация ключей подписи | JWS + JWKS |
| Согласованный каталог событий | AsyncAPI 3 |
| Не потерять событие после записи в БД | Transactional outbox → broker → webhook |
Чеклист перед production
- Trust store sandbox/prod разделены; тестовый cert не принимается в prod.
- JWKS содержит минимум два
kidпри ротации. - AsyncAPI и OpenAPI версионируются вместе (
info.version, changelog). - Outbox мониторится: возраст непубликованных строк, DLQ webhook.
- Пентест / 8.07.128 включает replay webhook и отсутствие
alg: noneв JWS.
Смежные материалы
| Тема | Статья |
|---|---|
| Партнёрский REST (заказы) | 7.06.117 |
| OAuth и HMAC-webhooks | 7.06.1171 |
| Outbox в Saga | 7.06.2124 |
| Теория очередей и брокеров | 2.09.121 |
| Kafka в интеграции | 2.09.123 |
| Маршрут по главам API | 7.06.117 |