Перейти к содержанию

SOAP, WSDL и WS-*

Стек классической SOA. SOAP задаёт формат сообщений, WSDL описывает контракт сервиса, WS-* добавляет безопасность, гарантии доставки и транзакции.

Аналогия: WSDL - меню ресторана (какие блюда есть и куда отдавать заказ), SOAP - бланк заказа строгой формы.

Зачем SOAP

  • Строгий контракт. Описание сервиса машиночитаемое, по нему генерируется клиент на любом языке. Сообщение неправильного формата отсекается проверкой схемы ещё до бизнес-логики.
  • Защита самого сообщения. Подпись и шифрование лежат внутри сообщения, а не только в канале. Подписанное сообщение можно хранить как юридически значимый документ.
  • Не зависит от транспорта. Одно и то же сообщение может идти по HTTP, через очередь JMS или через шину.
  • Готовые стандарты для гарантий. Гарантированная доставка и транзакции описаны стандартами, их не придумывают в каждом проекте заново.

Когда SOAP не нужен

  • новый публичный API, особенно для браузера и мобильных клиентов -- REST или gRPC проще и легче
  • внутренние сервисы одной команды: строгий контракт замедляет изменения, каждая правка ломает сгенерированных клиентов
  • большие объёмы и низкие задержки: XML многословный и медленно парсится

SOAP: формат сообщения

SOAP - протокол обмена сообщениями в формате XML. Стандартизирован W3C: SOAP 1.1 (2000), SOAP 1.2 (2003).

Из чего состоит сообщение

Envelope    - конверт, корневой элемент
├── Header  - необязательно: служебное (авторизация, подпись, ID транзакции)
└── Body    - сам вызов, ответ или ошибка (Fault)

Пример: запрос и ответ

Сервис банка «дай курс валюты». Запрос (SOAP 1.1 поверх HTTP):

POST /CurrencyService HTTP/1.1
Host: api.bank.example
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://bank.example/GetRate"

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:cur="http://bank.example/currency">
  <soap:Header>
    <cur:Auth>
      <cur:Token>abc123</cur:Token>
    </cur:Auth>
  </soap:Header>
  <soap:Body>
    <cur:GetRate>
      <cur:From>USD</cur:From>
      <cur:To>RUB</cur:To>
    </cur:GetRate>
  </soap:Body>
</soap:Envelope>

Ответ:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:cur="http://bank.example/currency">
  <soap:Body>
    <cur:GetRateResponse>
      <cur:Rate>92.15</cur:Rate>
      <cur:Date>2026-10-10</cur:Date>
    </cur:GetRateResponse>
  </soap:Body>
</soap:Envelope>

Ошибка - стандартный элемент Fault, обычно с HTTP 500:

<soap:Body>
  <soap:Fault>
    <faultcode>soap:Client</faultcode>          <!-- виноват клиент (Client) или сервер (Server) -->
    <faultstring>Unknown currency: XYZ</faultstring>
    <detail>...подробности в формате из WSDL...</detail>
  </soap:Fault>
</soap:Body>

Чем отличается от REST-запроса

  • Один URL на весь сервис, всегда POST. В REST действие задаётся URL и методом (GET /rates/USD). В SOAP сервер узнаёт, что делать, из Body (GetRate) и заголовка SOAPAction.
  • HTTP - только транспорт. SOAP не использует коды ответа, кеширование и методы HTTP. Он может идти и через JMS, и через SMTP.
  • Namespace'ы (xmlns:...) нужны, чтобы GetRate одного банка не путался с GetRate другого. Отсюда многословность SOAP.
  • Header - место для служебного: подпись, шифрование, ID транзакции. Тело запроса при этом не меняется.

WSDL: контракт сервиса

WSDL - XML-файл с описанием сервиса: какие у него операции, какие данные они принимают и возвращают, по какому адресу и как их вызывать.

Раздел WSDL Что описывает Аналогия из кода
types Структуры данных (на XML Schema, XSD) классы DTO
message Вход и выход каждой операции параметры и возвращаемый тип
portType Список операций интерфейс
binding Как передавать: версия SOAP, транспорт, стиль кодирования протокол
service / port Адрес сервиса URL

Первые три раздела описывают, что умеет сервис, последние два - как и где его вызвать.

Пример для того же сервиса курсов:

<definitions name="CurrencyService"
             targetNamespace="http://bank.example/currency"
             xmlns="http://schemas.xmlsoap.org/wsdl/"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns:tns="http://bank.example/currency"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <!-- 1. Типы данных -->
    <types>
        <xsd:schema targetNamespace="http://bank.example/currency" elementFormDefault="qualified">
            <xsd:element name="GetRate">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element name="From" type="xsd:string"/>
                        <xsd:element name="To" type="xsd:string"/>
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
            <xsd:element name="GetRateResponse">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element name="Rate" type="xsd:decimal"/>
                        <xsd:element name="Date" type="xsd:date"/>
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
        </xsd:schema>
    </types>

    <!-- 2. Сообщения: вход и выход -->
    <message name="GetRateRequest">
        <part name="parameters" element="tns:GetRate"/>
    </message>
    <message name="GetRateResponse">
        <part name="parameters" element="tns:GetRateResponse"/>
    </message>

    <!-- 3. Интерфейс: список операций -->
    <portType name="CurrencyPort">
        <operation name="GetRate">
            <input message="tns:GetRateRequest"/>
            <output message="tns:GetRateResponse"/>
        </operation>
    </portType>

    <!-- 4. Как передавать: SOAP поверх HTTP, document/literal -->
    <binding name="CurrencyBinding" type="tns:CurrencyPort">
        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="GetRate">
            <soap:operation soapAction="http://bank.example/GetRate"/>
            <input>
                <soap:body use="literal"/>
            </input>
            <output>
                <soap:body use="literal"/>
            </output>
        </operation>
    </binding>

    <!-- 5. Где: адрес сервиса -->
    <service name="CurrencyService">
        <port name="CurrencyPort" binding="tns:CurrencyBinding">
            <soap:address location="https://api.bank.example/CurrencyService"/>
        </port>
    </service>
</definitions>

Руками WSDL почти не пишут и не читают целиком - его генерируют инструменты.

Стили binding

  • rpc/encoded - старый стиль, типы зашиты прямо в сообщение. Плохо совместим между платформами, устарел.
  • document/literal - в Body лежит XML-документ, который проверяется по XSD из WSDL. Стандарт де-факто, его требует профиль совместимости WS-I Basic Profile.
  • document/literal wrapped - то же, но корневой элемент Body называется как операция (<GetRate>). Так делают почти все, включая пример выше.

Как этим пользуются

Код генерируется по контракту. Есть два подхода:

  • Code-first - пишешь класс, WSDL генерируется сам и доступен по адресу https://.../CurrencyService?wsdl.
  • Contract-first - сначала согласуют WSDL (например, банк и партнёр), потом обе стороны генерируют по нему код. В крупных интеграциях правильнее этот вариант: контракт - юридически значимая договорённость, и он не должен меняться от рефакторинга чьего-то класса.

Code-first на Java (JAX-WS):

@WebService
public class CurrencyService {
    @WebMethod
    public BigDecimal getRate(String from, String to) { ... }
}

Клиент, сгенерированный из WSDL, вызывает удалённый сервис как обычный метод:

CurrencyPort port = new CurrencyService().getCurrencyPort();
BigDecimal rate = port.getRate("USD", "RUB");   // внутри: XML -> HTTP POST -> XML -> объект

Инструменты:

  • Java - Apache CXF, Jakarta XML Web Services (Metro), Spring Web Services. Встроенный JAX-WS убрали из JDK в Java 11, теперь это отдельные зависимости.
  • .NET - dotnet-svcutil, «Add Service Reference».
  • Python - zeep.
  • Отладка - SoapUI: загружаешь WSDL, и он строит формы запросов для каждой операции.

WS-*: расширения поверх SOAP

WS-* - несколько десятков стандартов W3C и OASIS поверх SOAP. Каждый добавляет свой блок в Header, тело сообщения не меняется:

Envelope
├── Header
│   ├── wsa:*    - WS-Addressing: кому, от кого, куда отвечать
│   ├── wsrm:*   - WS-ReliableMessaging: номер сообщения в последовательности
│   └── wsse:*   - WS-Security: подпись, сертификат, токен
└── Body         - бизнес-запрос как есть

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

Атрибут mustUnderstand="true" на заголовке значит: не умеешь обрабатывать этот заголовок - не обрабатывай сообщение, верни ошибку. Так сервер не может молча пропустить проверку подписи.

Основные стандарты:

Стандарт Что даёт
WS-Security Подпись и шифрование самого сообщения, токены (логин, сертификат X.509, SAML), защита от повтора по времени
WS-Addressing ID сообщения, адрес для ответа и ошибки - асинхронный обмен
WS-ReliableMessaging Гарантированная доставка: без потерь, без дублей, по порядку
WS-AtomicTransaction Распределённые транзакции по 2PC
WS-BusinessActivity Долгие процессы с компенсациями - по сути стандартизованная saga
WS-Policy Требования сервиса прямо в WSDL: «подписывать сообщения», «шифровать Body»
WS-Trust, WS-Federation Выдача токенов и единый вход между организациями
MTOM Передача файлов отдельной частью сообщения, без base64 внутри XML

WS-Security и HTTPS

HTTPS (TLS) WS-Security
Что защищает Канал между двумя точками Само сообщение
Через посредников На каждом узле расшифровывается заново Подпись проверяется от отправителя до получателя
После доставки Защиты больше нет Подписанное сообщение можно хранить как доказательство
Юридическая значимость Нет Да, если подпись квалифицированная

Поэтому банки и госсистемы держатся за WS-Security: подписанное сообщение - это электронный документ.

Чем WS-* заменили сейчас

WS-* Современный аналог
WS-Security TLS / mTLS. Для защиты самого сообщения - JWS и JWE
WS-Trust, WS-Federation OAuth 2.0, OpenID Connect, SAML
WS-Addressing HTTP-заголовки, correlation ID, заголовки сообщений брокера
WS-ReliableMessaging Kafka, RabbitMQ с подтверждениями, идемпотентность, outbox
WS-AtomicTransaction Saga и outbox вместо распределённых транзакций
WS-Policy securitySchemes в OpenAPI
MTOM multipart/form-data, presigned URL на объектное хранилище

Семейство критикуют за размер и сложность: десятки пересекающихся спецификаций, подпись XML ломается от лишнего пробела, реализации разных вендоров плохо совместимы между собой. Из всего набора на практике чаще всего встречаются WS-Security, WS-Addressing и MTOM.

Где SOAP встречается сейчас

Новые публичные API на SOAP почти не делают, но в интеграциях он встречается постоянно:

  • госсистемы РФ - СМЭВ, ФНС, Росреестр: сообщения подписываются XML-подписью по ГОСТ
  • банки и платёжные системы - межбанковские интеграции, процессинг
  • корпоративный софт - SAP, 1С, Oracle, Microsoft Dynamics, WCF в .NET
  • Windows-инфраструктура - WinRM (удалённый PowerShell) работает на WS-Management
  • IP-камеры ONVIF - SOAP, WS-Discovery и WS-Security UsernameToken

SOAP и REST

SOAP + WSDL REST + JSON
Что это Протокол со строгими правилами Архитектурный стиль
Формат Только XML Обычно JSON
Контракт WSDL, обязателен OpenAPI, по желанию
Адресация Один URL, всегда POST Ресурсы в URL и HTTP-методы
Ошибки soap:Fault HTTP-коды
Кеширование HTTP Не используется Работает для GET
Безопасность WS-Security на уровне сообщения TLS и OAuth/JWT на уровне канала
Гарантии доставки, транзакции Стандарты WS-* Решаются отдельно: идемпотентность, outbox, saga
Генерация клиента Из коробки Через генераторы по OpenAPI