Перейти к содержимому

Веб-сервис SOAP на сайте - описание, WSDL и авторизация

Внешнее приложение умеет только SOAP и просит контракт с описанием методов. Поднимаем на сайте веб-сервис: описание, WSDL, авторизация по HTTP Basic и бинарные данные. Заодно решаем, оправдан ли этот механизм в новом коде.

Механика

Здесь сайт выступает сервером, а внешняя программа - клиентом. Вызов приходит запросом SOAP, платформа находит метод сервиса и возвращает ответ тем же протоколом.

Механизм легаси и живёт в отдельном модуле «Веб-сервисы». Сервис описывается компонентом на публичной странице, а класс сервиса наследуется от IWebService.

Описание отдаёт метод GetWebServiceDesc(), возвращающий объект CWebServiceDesc. В нём перечислены имя сервиса, класс-обработчик и состав параметров каждого метода.

Описание - единственный источник контракта, и по нему собирается WSDL. Само описание клиент забирает по адресу страницы сервиса с параметром ?wsdl и генерирует по нему свой код.

Авторизация здесь HTTP Basic: логин и пароль пользователя сайта приходят заголовком запроса. Сессия браузера в этой схеме не участвует, поэтому проверка сервиса из браузера ничего не доказывает.

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

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

Отказ отдают объектом CSOAPFault, а не текстом ошибки внутри успешного ответа. Так клиент отличает сбой от результата, не разбирая содержимое ответа глазами.

Протокол переносит XML, поэтому бинарные данные едут в Base64. Клиент кодирует файл, сервер декодирует его и только потом сохраняет штатными средствами.

Шаги

  1. Убедиться, что расширение soap включено в PHP и на боевом сервере, и на стенде.
  2. Завести класс-наследник IWebService и описать в нём сервис вместе с методами.
  3. Поставить компонент сервиса на публичную страницу: её адрес и станет точкой вызова.
  4. Открыть адрес с параметром wsdl и проверить, что описание отдаётся клиенту целиком.
  5. Позвать метод с логином и паролем по HTTP Basic и разобрать ответ как настоящий клиент.

Код

Проверяем расширение soap в PHP:

Окно терминала
php -m | grep -i soap # пусто: расширения нет, сервис не поднимется
yum -y install php-soap && systemctl restart httpd.service # BitrixVM, разово
php -m | grep -i soap # после перезапуска строка soap появляется в списке

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

Поднимаем страницу сервиса:

// /ws_addnews.php - публичная страница, её адрес и есть точка вызова
require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
\Bitrix\Main\Loader::includeModule('webservice'); // модуль «Веб-сервисы»
// на странице стоит компонент сервиса: он принимает вызовы и отдаёт описание
require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php');

Адрес страницы попадает в настройки внешнего приложения и меняется потом с большим трудом. Выбирают его один раз и не двигают вместе с перестройкой разделов сайта.

Описываем сервис и его методы:

class CAddNewsWS extends IWebService // базовый класс модуля «Веб-сервисы»
{
public function GetWebServiceDesc() // единственная точка описания контракта
{
$desc = new CWebServiceDesc();
// здесь перечисляются имя сервиса, класс-обработчик и параметры методов
return $desc; // по описанию платформа соберёт WSDL
}
}

Правка описания меняет WSDL, а вместе с ним и сгенерированный код на стороне клиента. Состав параметров поэтому согласуют до первого вызова, а не после запуска обмена.

Пишем метод сервиса:

public function AddNews($NAME, $DATE, $PREVIEW_TEXT, $DETAIL_TEXT, $KEYWORDS, $SOURCE)
{
// права на инфоблок проверяем до записи: вызов пришёл мимо форм админки
$el = new CIBlockElement();
$id = $el->Add(['IBLOCK_ID' => 12, 'NAME' => $NAME, 'ACTIVE_FROM' => $DATE,
'PREVIEW_TEXT' => $PREVIEW_TEXT, 'DETAIL_TEXT' => $DETAIL_TEXT,
'PROPERTY_VALUES' => ['KEYWORDS' => $KEYWORDS, 'SOURCE' => $SOURCE]]);
// текст отказа лежит в $el->LAST_ERROR, и его отдают клиенту, а не пишут в лог
return ['id' => $id]; // при отказе вместо массива возвращают объект CSOAPFault
}

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

Принимаем картинку в Base64:

$binary = base64_decode($PHOTO); // клиент кодирует файл, сервер декодирует
$tmp = $_SERVER['DOCUMENT_ROOT'] . '/upload/tmp/' . uniqid() . '.jpg';
file_put_contents($tmp, $binary);
$fields['DETAIL_PICTURE'] = \CFile::MakeFileArray($tmp); // массив как из формы загрузки
// временный файл убираем после сохранения элемента, иначе папка загрузок растёт

Base64 увеличивает объём примерно на треть, и на крупных файлах вызов упирается в пределы PHP. Картинки поэтому чаще передают ссылкой, а по ней сервер забирает файл сам.

Смотрим описание сервиса:

Окно терминала
curl -s "https://example.com/ws_addnews.php?wsdl" | head -5
# в ответе ждём xml с описанием методов, а не html обычной страницы сайта
# пустой ответ на этом шаге - почти всегда выключенное расширение soap

Зовём метод с авторизацией:

Окно терминала
curl -s -u login:password -H 'Content-Type: text/xml' \
--data @request.xml "https://example.com/ws_addnews.php"
# ответ 401 означает, что заголовок Basic до сервиса не дошёл

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

Настраиваем клиента на .NET:

<basicHttpBinding>
<binding name="bitrixWs">
<security mode="TransportCredentialOnly"> <!-- учётные данные идут транспортом -->
<transport clientCredentialType="Basic" /> <!-- логин и пароль пользователя сайта -->
</security>
</binding>
</basicHttpBinding>

Windows-клиент по умолчанию ждёт защищённый транспорт и свои механизмы проверки подлинности. Basic поверх обычного HTTP включают явно, а сам вызов при этом уводят на HTTPS.

Почему это не первый выбор для нового кода

SOAP на сайте остался ради приложений, которые другого протокола не знают. Новый клиент почти всегда умеет обычный HTTP с JSON, и городить XML-контракт ради него незачем.

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

Когда наружу нужен свой формат и свои токены, задачу закрывает своё API на контроллере. Там же решается версионирование, которого у автоматически собранного описания нет вовсе.

Ограничения

Модуль веб-сервисов давно не развивается, и это видно по возможностям описания. Собранный автоматически WSDL покрывает простые типы, а вложенные структуры чужого стандарта в нём не выражаются.

Обратное направление здесь не рассматривается: сайт как клиент SOAP - отдельная задача. Она решается обычным классом SoapClient из PHP, и платформа в ней не участвует ничем.

Совместимость с 1С проверяют на настоящем клиенте, а не на удобном тестере запросов. Разбор XML в 1С строже, и ответ, устраивающий тестер, разваливается там на преобразовании типов.

Сервис отвечает на том же сайте и той же нагрузке, что и витрина магазина. Долгие выгрузки поэтому режут на страницы, иначе вызов упирается в предел времени выполнения PHP.

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

Типичные проблемы

Из браузера сервис работает, а клиент получает 401.

Запрос пришёл без заголовка HTTP Basic, и до выполнения метода дело не дошло. Логин и пароль пользователя сайта клиент передаёт заголовком при каждом вызове.

В 1С ошибка преобразования данных XDTO.

Метод вернул строку вместо массива, описанного в его выходном параметре. Возвращать нужно массив с тем ключом, который назван в описании сервиса.

Страница сервиса открывается пустой, описание не отдаётся.

На этом сервере не включено расширение soap для PHP, и компонент выводит пустоту. Тот же файл на соседнем сервере работает, поэтому причину ищут не в коде.

Картинка приходит битой или обрывается на середине.

Бинарные данные ушли в XML как есть, без кодирования в Base64. Клиент кодирует файл перед отправкой, а сервер декодирует его до сохранения.

Сложную структуру ответа не удаётся описать в WSDL.

Автоматическая сборка описания рассчитана на простые типы и плоские массивы. Вложенные структуры чужого стандарта в ней не выражаются, и задачу решают сторонней библиотекой.

Через сервис в инфоблок пишет кто угодно.

Метод сохраняет данные, не проверяя права учётной записи на инфоблок. Вызов приходит мимо форм админки, поэтому проверку прав пишут в самом методе.

Частые вопросы

Как создать веб-сервис в Битриксе?

Классом-наследником IWebService с методом GetWebServiceDesc(), который возвращает описание сервиса и его методов. Сам сервис выводится компонентом на публичной странице, а описание WSDL отдаётся по её адресу с параметром ?wsdl.

Как правильно авторизоваться, чтобы заработал веб-сервис?

Авторизация здесь HTTP Basic: клиент передаёт логин и пароль пользователя сайта заголовком запроса. Ответ 401 означает, что заголовок не пришёл, а проверка из браузера работает на сессии и потому ничего не доказывает.

Работает ли модуль веб-сервисов на ограниченной лицензии?

Работает: ограничение редакции здесь ни при чём. Пустая страница вместо описания почти всегда означает выключенное расширение soap в PHP на этом сервере.

Как реализовать SOAP-клиент на Битриксе?

Это обратное направление, и платформа в нём не участвует: подходит обычный класс SoapClient из PHP. Адрес и имена методов берут из WSDL той системы, которую вызывают.

Делать ли новую интеграцию с 1С через веб-сервисы?

Обычно нет: штатный обмен и REST закрывают задачу дешевле и поддерживаются. SOAP оставляют там, где внешнее приложение другого протокола не понимает.

Смежное

Первоисточник