Disclaimer: Статей на эту тему написано очень много и, как вы, конечно, догадались, это очередная. Возможно, вы узнаете из неё что-то новое, но ничего сверхсекретного, что нельзя было бы нагуглить самостоятельно, здесь не описано. Только заметки из личного опыта.
Вступление
Рассматривать будем только ситуацию, когда есть сторонний web-сервис и стоит задача наладить обмен данными.
Строение сервиса описывается в файле WSDL (англ. Web Services Description Language)
Файл чаще всего доступен по ссылке, где находится точка входа в сам web-сервис. Я написал «чаще всего», так как бывают исключения. Например, Web-сервис на базе SAP не публикует wsdl и его можно получить только выгрузив из самого приложения.
И так, у нас есть описание web-сервиса, логин, пароль. Давайте подключимся.
// Определяем настройки
URLПространстваИменСервиса = "http://Somesite.ru";
ИмяПользователя = "TestUser";
Пароль = "q1w2e3";
МестоположениеWSDL = "https://Somesite.ru/WebService/Some?wsdl";
ИмяСервиса = "SomeServiceName";
ИмяТочкиПодключения = "SomeService_Port";
// Создаем подключение
SSL = Новый ЗащищенноеСоединениеOpenSSL();
WSОпределение = Новый WSОпределения(МестоположениеWSDL,,,,,SSL);
WSПрокси = Новый WSПрокси(WSОпределение, URLПространстваИменСервиса, ИмяСервиса, ИмяТочкиПодключения,,,SSL);
WSПрокси.Пользователь = ИмяПользователя;
WSПрокси.Пароль = Пароль;
Отлично! Мы подключились к web-сервису! По идее это основа любого варианта обмена, так как позволяет создавать объект структуры данных на основании wsdl, а работать с таким объектом одно удовольствие.
Рассмотрим XML который нам выдает SoapUI
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:som="http://Somesite.ru">
<soapenv:Header/>
<soapenv:Body>
<som:SUBMISSION>
<som:ID>357</som:ID>
<som:CUSTOMER>
<som:CLIENT_ID>121212</som:CLIENT_ID>
<som:SEX>M</som:SEX>
<som:CLIENT_BIRTHDAY>19900111</som:CLIENT_BIRTHDAY>
<som:CARS>
<som:CLASS>Mercedes</som:CLASS>
<som:MODEL>GLS</som:MODEL>
</som:CARS>
<som:CARS>
<som:CLASS>Audi</som:CLASS>
<som:MODEL>TT</som:MODEL>
</som:CARS>
</som:CUSTOMER>
</som:SUBMISSION>
</soapenv:Body>
</soapenv:Envelope>
Теперь опишем его программно
// Создаем объект и наполняем его данными
СвояФабрикаXDTO = WSОпределение.ФабрикаXDTO;
КорневойТип = СвояФабрикаXDTO.Тип(URLПространстваИменСервиса, "SUBMISSION");
КорневойОбъект = СвояФабрикаXDTO.Создать(КорневойТип);
КорневойОбъект.ID = "4356";
КлиентТип = СвояФабрикаXDTO.Тип(URLПространстваИменСервиса, "CUSTOMER");
КлиентОбъект = СвояФабрикаXDTO.Создать(КлиентТип);
КлиентОбъект.CLIENT_ID = "121212";
КлиентОбъект.SEX = "M"; // F - женский, M - мужской
КлиентОбъект.CLIENT_BIRTHDAY = "19900111";
// Автомобили клиента
АвтоТип = СвояФабрикаXDTO.Тип(URLПространстваИменСервиса, "CARS");
АвтоОбъект = СвояФабрикаXDTO.Создать(АвтоТип);
АвтоОбъект.CLASS = "Mercedes";
АвтоОбъект.MODEL = "GLS";
КлиентОбъект.CARS.Добавить(АвтоОбъект);
АвтоОбъект = СвояФабрикаXDTO.Создать(АвтоТип);
АвтоОбъект.CLASS = "Audi";
АвтоОбъект.MODEL = "TT";
КлиентОбъект.CARS.Добавить(АвтоОбъект);
КорневойОбъект.CUSTOMER.Добавить(КлиентОбъект);
Данные успешно заполнены. Теперь нужно их отправить.
В этот самый момент и возникает множество нюансов. Попробуем рассмотреть каждый.
Рецепт 1. Отправляем XDTO-объект целиком
Результат = WSПрокси.AddCustomers(КорневойОбъект);
Остается лишь обработать результат, который нам вернул сервис и на этом всё. Согласитесь, что это очень удобно!
Но на практике не всегда бывает так. Например 1с не ладит с префиксацией определенных тэгов внутри xml, когда пространство имен корнеового тэга отличается от пространства дочерних. В таких случаях приходится собирать soap вручную. Так же приходилось сталкиваться с web-сервисами, которые в качестве параметра ждут xml в чистом виде. Маразм, но все же делается это не слишком сложно.
Рецепт 2. Отправляем чистый xml в качестве параметра
ПараметрыЗаписиXML = Новый ПараметрыЗаписиXML("UTF-8", "1.0", Истина);
МойXML = Новый ЗаписьXML;
МойXML.УстановитьСтроку(ПараметрыЗаписиXML);
МойXML.ЗаписатьОбъявлениеXML();
СвояФабрикаXDTO.ЗаписатьXML(МойXML, КорневойОбъект);
СтрокаXML = МойXML.Закрыть();
Если УдалитьОписаниеПространстваИмен Тогда
Попытка
ПервыйТэгВСтроке = СтрПолучитьСтроку(СтрокаXML,2);
ИмяКорневогоТэга = КорневойОбъект.Тип().Имя;
СтрокаXML = СтрЗаменить(СтрокаXML, ПервыйТэгВСтроке, "<"+ИмяКорневогоТэга+">");
Исключение
//ОписаниеОшибки()
КонецПопытки;
КонецЕсли;
Результат = WSПрокси.AddCustomers(СтрокаXML);
Если не удалять пространство имен, которое 1с добавляет по умолчанию, то стало больше всего на 5 строк кода. Чаще всего я заворачиваю преобразование xml в функцию, так как обычно вызываем более одного метода.
Рецепт 3. Отправляем через нативный HTTPЗапрос.
СтрокаSOAP = "<soapenv:Envelope xmlns:soapenv=""http://schemas.xmlsoap.org/soap/envelope/"" xmlns=""http://Somesite.ru"">
| <soapenv:Header/>
| <soapenv:Body>
|"
+СтрокаXML+
"
| </soapenv:Body>
|</soapenv:Envelope>";
// Описываем заголовки HTTP-запроса
Заголовки = Новый Соответствие;
Заголовки.Вставить("Content-Type", "text/xml;charset=UTF-8");
Заголовки.Вставить("SOAPAction", "http://sap.com/xi/WebService/soap1.1");
Заголовки.Вставить("Authorization", "Basic "+ПолучитьBase64ЗаголовокАвторизации(ИмяПользователя, Пароль));
// ВНИМАНИЕ!!!
// Нельзя заполнять программно следующие заголовки, так как это приводит к ошибке
// Платформа сама правильно их заполнит
//Заголовки.Вставить("Accept-Encoding", "gzip,deflate");
//Заголовки.Вставить("Content-Length", Формат(СтрДлина(СтрокаSOAP),"ЧГ=")); // длина сообщения
//Заголовки.Вставить("Host", "Somesite.ru:8001");
//Заголовки.Вставить("Connection", "Keep-Alive");
//Заголовки.Вставить("User-Agent", "Apache-HttpClient/4.1.1 (java 1.5)");
// Подключаемся к сайту.
Соединение = Новый HTTPСоединение("Somesite.ru/WebService/Some",,ИмяПользователя, Пароль,,,SSL, Ложь); // Адрес должен быть без https://
// Получаем текст корневой страницы через POST-запрос.
HTTPЗапрос = Новый HTTPЗапрос("/GetCustomer", Заголовки);
HTTPЗапрос.УстановитьТелоИзСтроки(СтрокаSOAP);
Результат = Соединение.ВызватьHTTPМетод("POST", HTTPЗапрос);
В этом варианте нам придется собрать soap вручную. По сути мы просто оборачиваем xml из рецепта 2 в оболочку soap, где в зависимости от требований web-сервиса мы можем менять наш soap как душе угодно.
Далее описываем заголовки согласно документации. Некоторые сервисы спокойно прожуют наш запрос и без заголовков, тут надо смотреть конкретный случай. Если вы не знаете какие заголовки прописывать, то самый простой способ это подглядеть запрос в SoapUI переключившись во вкладку RAW.
Функция получения Base64 строки выглядит так (подсмотрел здесь):
Функция ПолучитьBase64ЗаголовокАвторизации(ИмяПользователя, Пароль)
КодировкаФайла = КодировкаТекста.UTF8;
ВременныйФайл = ПолучитьИмяВременногоФайла();
Запись = Новый ЗаписьТекста(ВременныйФайл, КодировкаФайла);
Запись.Записать(ИмяПользователя+":"+Пароль);
Запись.Закрыть();
ДвДанные = Новый ДвоичныеДанные(ВременныйФайл);
Результат = Base64Строка(ДвДанные);
УдалитьФайлы(ВременныйФайл);
Результат = Сред(Результат,5);
Возврат Результат;
КонецФункции
Есть важный момент. При работе с HTTPСоединение указывайте адрес без указания протоколов «http://» и «https://», иначе рискуете потратить время на поиск не очевидной ошибки.
Рецепт 4. Отправляем через WinHttpRequest
WinHttp = Новый COMОбъект("WinHttp.WinHttpRequest.5.1");
WinHttp.Option(2,"UTF-8");
WinHttp.Option(4, 13056); //intSslErrorIgnoreFlag
WinHttp.Option(6, true); //blnEnableRedirects
WinHttp.Option(12, true); //blnEnableHttpsToHttpRedirects
WinHttp.Open("POST", "https://Somesite.ru/WebService/Some/GetCustomer", 0);
WinHttp.SetRequestHeader("Content-type", "text/xml");
WinHttp.SetCredentials(ИмяПользователя, Пароль, 0);
WinHttp.Send(СтрокаSOAP);
WinHttp.WaitForResponse(15);
XMLОтвет = WinHttp.ResponseText();
Здесь по сути тоже самое, что и в предыдущем варианте, но работаем с COMОбъектом. Строку соединения указываем полностью, вместе с протоколом. Особое внимание следует уделить только флагам игнорирования ошибок SSL-сертификатов. Они нужны, если мы работаем по SSL, но без определенного сертификата, так как создать новое защищенное соединение в таком варианте не предоставляется возможным (или я не умею как). В остальном механизм схож с предыдущим.
Так же помимо "WinHttp.WinHttpRequest.5.1" можно использовать "Microsoft.XMLHTTP", "Msxml2.XMLHTTP", "Msxml2.XMLHTTP.3.0", "Msxml2.XMLHTTP.6.0", если вдруг не взлетит на WinHttp. Методы практически такие же, только количество параметров другое. Подозреваю, что один из этих вариантов и зашит внутри объекта 1c HTTPЗапрос.
На данный момент это все рецепты, что у меня есть. Если столкнусь с новыми, то обязательно дополню статью.
Обработка результата
В рецепте 1 мы чаще всего получаем готовый XDTO-объект и работаем с ним как со структурой. Во всех остальных случаях можно преобразовывать xml-ответ в XDTO
Если Результат.КодСостояния = 200 Тогда
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(Результат.ПолучитьТелоКакСтроку());
ОбъектОтвет = СвояФабрикаXDTO.ПрочитатьXML(ЧтениеXML);
Сообщить(ОбъектОтвет.Body.Response.RESPONSE_ID);
Сообщить(ОбъектОтвет.Body.Response.RESPONSE_TEXT);
КонецЕсли;
Тут все просто.
Вместо заключения
1. Начинайте работу с web-сервисами с программы SoapUI. Она предназначена для таких работ и позволит быстрее понять как работает тот или иной сервис. Для освоения есть статья про SoapUI.
2. Если вы обмениваете с сервисом по незащищенному каналу http и возникает вопрос в том что именно отправляет 1с в своих сообщениях, то можно воспользоваться снифферами трафика такими как Wireshark, Fiddler, и другие. Проблема возникнет только если используете ssl-соединение.
3. Если все же web-сервис общается по https, то ставим на удаленной машине (любой, главное не на своей) сервер Nginx, к которому мы и будем обращаться, а он в свою очередь запакует все в https и перешлет куда нужно (reverse proxy) и в стандартный конфиг добавляем:
server {
listen 0.0.0.0:8080;
server_name MyServer;
location ~ .* {
proxy_pass https://Somesite.ru:8001;
proxy_set_header Host $host;
proxy_set_header Authorization "Basic <base64 ваш пароль:логин>";
proxy_pass_header Authorization;
}
}
4. Если вас пугает XDTO, то рекомендую ознакомится с циклом статей злого бобра Андрея XDTO — это просто.
5. Если аутентификация предполагает использование Cookie, то нашлась следующая статья.
P.S. Если у вас появились вопросы, предложения по улучшению кода, есть собственные рецепты, отличные от описанных, вы нашли ошибки или считаете, что автор "ниправ" и ему "не место в 1с", то пишите комментарии, и мы все обсудим.
UPD:
1. Добавил по просьбе join2us XML, который выдавал SoapUI
2. Исправил ошибки найденные пользователем VasilVtoroy
Через Altova XML spy еще удобно тестить, она и поживее чем Soap UI. Интерфейс такой не дает конечно) Но на практике обычно надо было посылать готовый запрос, а не вбивать его вручную.
, fiddler — очень удобно, можно работать и с ssl/tls (Tools-Options-HTTPS), а вот Wireshark для этой задачи не так удобен.
(0)
вот на этом месте можно подробнее? И желательно с примерами, если вдруг остались под рукой.
(3) Да. Веб-сервис ждет на входе xml следующего вида:
Показать
Обратите внимание на префикс cus в версии от SoapUI
И теперь вариант от 1с:
Показать
т.е. Пространство имен «http://SomeSite.org/customer» должно распространяться только на элемент CustomerCore, а не на всю ветку.
(4) Ну, это вообще косяк принимающей стороны, ибо эти два XML равнозначны по своей сути. И 1С тут нисколько не лукавит, создавая такой запрос.
(5) Согласен, но изменить принимающую сторону бывает невозможно и приходится подстраиваться. К тому же SoapUI интерпретировал WSDL ровно так, как того ждал принимающий веб-сервис
(6)
с этим не поспоришь.
Но лучше обойтись штатными способами:
вместо
Показать
сделать честные ЗаписатьНачалоЭлемента(«Envelope»); Потом ЗаписатьСоответствиеПространствИмен(«cus», «http://SomeSite.org/customer»);
и уже потом в эту же запись XML записать фабрикой необходимый объект. Потом закрыть запись и добавить три пробела, если уж принимающая сторона настолько упорота.
Всё же, пилить XML текстом — неблагодарное дело.
(7) Но это же тоже самое. Строку xml я формирую не сам, а получаю из фабрики. Опять же, не претендую на правду в последней инстанции.
(8) я про разделы Envelope и Body. Если их записать штатными средствами платформы и в них же туда прописать соответствие пространств имён с нужным префиксом, то фабрика при записи объекта уже не будет совать xmlns, так как он уже будет не нужен.
(9) Видимо я не понял суть решения. То есть создаем объект ЗаписьXML, прописываем Envelope, Body. Отлично. Как потом дружим их с фабрикой?
(10) точно так же:
где перед этим кодом запись начал элементов Envelope и Body с указанием пространств имён, а после этого конца, соответственно, закрытие элементов Body и Envelope.
Есть веб сервис который на вход принимает ID, KEY (string)
Смотрю через SoapUI структура
<soap:Header/>
<soap:Body>
<api2:AllCatalog>
<api2:AllCatalogRequest>
<int:Auth>
<int:ID></int:ID>
<int:KEY></int:KEY>
</int:Auth>
</api2:AllCatalogRequest>
</api2:AllCatalog>
</soap:Body>
</soap:Envelope>
Показать
как данную структуру описать из 1С?
(12) Сейчас 1с неправильно формирует? Раз через SoapUI работаете, то описание веб сервиса (wsdl) у вас есть. Попробуйте так как описано в статье (Вступление и рецепт 1). У вас соответственно будет вот так:
Показать
(13)разобрался, спасибо
1) Content-Length устанавливать не надо.
Платформа сама его правильно посчитает (в отличие от приведенного кода)
2) Fiddler тоже позволяет перехватывать защищенные http-соединения
3) Заголовок Host тоже не надо устанавливать
4) Basic-авторизацию можно делать не вручную, а задав в HTTPСоединения свойства логин и пароль.
Таким образом вы и Windows аутентификацию можете делать.
5) Accept-Encoding устанавливать просто нельзя, платформа не поддерживает сжатие HTTP!
(15) Заголовки я не сам придумал. Это было одним из требованием принимающей стороны. По поводу Fiddler`а согласен, но я поздно узнал, что он умеет перехватывать https.
(16) Я же не говорю, что он не нужны.
Но пункты (1) и (3) платформа формирует сама, и более правильно
По пункту (4) — тоже самое можно сделать проще
По пункту (5) — такого требования точно быть не могло. Если бы на другой стороне был сервер с поддержкой сжатия, то вы бы получили чушь вместо ответа
Спасибо, за информацию. Статья очень помогла для настройки подключения к SilverDat.
(18) Это здорово. Были какие-то особенности при настройке обмена?
(4)Здравствуйте. Вопрос не по теме, но относительно вашего комментария.
Я создаю WSПрокси, заполняю параметры и вызываю метод GetReferenceData.
Показать
Каким образом я могу сначала посмотреть xml запрос, который отправляется? Просто возникает ошибка, а отладить не получается.
(20) Можно преобразовать обратно из фабрики в XML
Показать
Но лучше всего воспользоваться сниферами типа Fiddler, Wireshark и т.д. Такой метод будет более точным.
(21) Спасибо. Fiddler помог. Нашел единственное отличие. Почему-то 1С не формирует тег <soap:Header/>.
Должно быть
<soap:Envelope xmlns:soap=»http://schemas.xmlsoap.org/soap/envelope/»>
<soap:Header/>
<soap:Body>
<m:GetReferenceData xmlns:m=»http://www.cargo3.ru»>
а у меня
<soap:Envelope xmlns:soap=»http://schemas.xmlsoap.org/soap/envelope/»>
<soap:Body>
<m:GetReferenceData xmlns:m=»http://www.cargo3.ru»>
где искать причины?
(22) Это заголовки. Я не знаю как насильно прописать заголовок в XML при работе с фабрикой. Вообще по идее он никак не должен влиять на поведение системы. Уверены, что наличие <soap:Header/> изменит ситуацию?
// Создаем объект и наполняем его данными
Вот честное слово! Автор, если бы Вы перед созданием объекта в 1С привели пример хмл из SoapUI — нубам было бы в разы проще усвоить корректный синтаксис…
ЗЫ: в любом случае, спасибо за инфо
(24) Вы правы. Мое упущение. Сомневаюсь, что найду эту xml, но попробую сделать аналог, чтобы было понятнее.
SoapUI хорошая тулза, но с неприятным для нашей работы недостатком. Она не переваривает кириллицу, ни в запросах ни в ответах ни в авторизации.
(26) Может вы пробовали старую версию? Сейчас она прекрасно переваривает в любых направлениях