Обновлено 23.05.2023: Актуализировал процедуру формирования запроса (добавлена канонизация СМЭВ, форматирование XML и прочее)
В статье по шагам рассмотрена процедура обмена с ФГИС "Зерно" через API. Для примера получим справочник "ОКПД 2" и создадим пробную партию зерна. Предметная область, интеграция с рабочей конфигурацией и прочие связанные вещи рассматриваться не будут.
Итак, у нас есть официальный сайт https://specagro.ru/fgis, на котором можно скачать файл с документацией по API, попасть в личный кабинет (в т. ч. тестовый), ну и вообще посмотреть на картинки. Внутри файла лежат непосредственно документация, XSD-схемы, которые нам очень понадобятся. Перед началом рекомендую ознакомиться с документацией, многие подробности можно найти в ней. На момент написания статьи актуальная версия API 1.0.3. Схемы прикреплены файлом к статье.
Для обмена с "Зерном" необходимы сертификат безопасности, с помощью которого мы будем подписывать наши сообщения, и установленный криптопровайдер. Предварительно сертификат (открытый ключ) необходимо загрузить в личный кабинет (через меню "Моя организация" -> "Информационная безопасность").
Также нужна будет компонента ExtraCryptoAPI (входит в состав БСП).
Для формирования и обработки запроса на XML будем использовать фабрику XDTO.
Приступим. Для начала попробуем получить из системы справочник "ОКПД 2". Для этого нам нужно:
- Создать фабрику XDTO
- Сформировать запрос
- Подписать сообщение с запросом ЭЦП
- Отправить запрос и получить ответ
1. Создание фабрики XDTO
Для создания фабрики нам будут нужны XSD-схемы, которые можно взять из архива с документацией (на скриншоте старая версия, но суть та же):
Как видно, в основном схемы представлены попарно: с префиксом "api" и без него. В первом случае в файле описаны схемы запросов для работы с объектами, во втором случае - структуры самих объектов.
Для начального примера будут нужны три файла: "fgis-zerno-api-dictionaries-1.0.3.xsd", "fgis-zerno-dictionary-1.0.3.xsd" и "fgis-zerno-api-types-1.0.3.xsd". Первые два необходимы для работы справочников, третий описывает служебные объекты.
Загрузим файлы в макет и создадим нашу фабрику:
МассивПутей = Новый Массив;
КаталогВременныхФайлов = КаталогВременныхФайлов();
ИмяФайла = КаталогВременныхФайлов + "fgis-zerno-api-types-1.0.3.xsd";
Макет = ПолучитьМакет("fgis_zerno_api_types"); // Макет с файлом "fgis-zerno-api-types-1.0.3.xsd"
Макет.Записать(ИмяФайла);
МассивПутей.Добавить(ИмяФайла);
ИмяФайла = КаталогВременныхФайлов + "fgis-zerno-api-dictionaries-1.0.3.xsd";
Макет = ПолучитьМакет("fgis_zerno_api_dictionaries"); // Макет с файлом "fgis-zerno-api-dictionaries-1.0.3.xsd"
Макет.Записать(ИмяФайла);
МассивПутей.Добавить(ИмяФайла);
ИмяФайла = КаталогВременныхФайлов + "fgis-zerno-dictionary-1.0.3.xsd";
Макет = ПолучитьМакет("fgis_zerno_dictionary"); // Макет с файлом "fgis-zerno-dictionary-1.0.3.xsd"
Макет.Записать(ИмяФайла);
МассивПутей.Добавить(ИмяФайла);
Фабрика = СоздатьФабрикуXDTO(МассивПутей);
2. Формирование запроса
Ниже код формирования запроса XML на получение справочника "ОКПД 2":
// Создаем объект XDTO, в параметрах указываем пространство имен и имя типа
// Оба значения можно найти в файле XSD-схемы, либо посмотреть отладкой в объекте Фабрика
ОбъектRequest = Фабрика.Создать(Фабрика.Тип("urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/dictionaries/1.0.3",
"DictionaryRequestType"));
// Указываем название справочника - "OKPD2". Список справочников можно найти в файле "fgis-zerno-dictionary-1.0.3.xsd"
ОбъектRequest.Dictionary = "OKPD2";
МойXML = Новый ЗаписьXML;
МойXML.УстановитьСтроку(Новый ПараметрыЗаписиXML("UTF-8", "1.0", Истина));
// Записываем наш объект в виде XML. Имя элемента "Request" можно узнать из документации или отладкой
Фабрика.ЗаписатьXML(МойXML, ОбъектRequest, "Request");
Request = МойXML.Закрыть();
В переменной Request после выполнения кода будет содержаться следующий текст:
<Request xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/dictionaries/1.0.3" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Dictionary>OKPD2</Dictionary>
</Request>
Это и есть наш запрос, который мы отправим в систему. Теперь его необходимо "обернуть" и подписать с помощью сертификата.
3. Подписание сообщения
Получившийся запрос поместим в еще один XML, "конверт", соответствующий формату протокола SOAP.
В шаблоне конверта между знаками "%" заключены параметры, вместо которых мы будем подставлять нужные значения:
КонвертXML = КонвертXML(); // Шаблон итогового текста запроса
MessageID = Строка(Новый УникальныйИдентификатор); // Уникальный идентификатор сообщения
ReferenceMessageID = MessageID; // Идентификатор сообщения, на которое ссылаемся. В случае отправки запроса совпадает с MessageID
// Необходимо заключить запрос в специальные теги
MessagePrimaryContent =
"<ns:MessagePrimaryContent>
| %Request%
|</ns:MessagePrimaryContent>";
MessagePrimaryContent = СтрЗаменить(MessagePrimaryContent, "%Request%", Request);
// Заполняем параметры
КонвертXML = СтрЗаменить(КонвертXML, "%MessagePrimaryContent%", MessagePrimaryContent); // Собственно запрос
КонвертXML = СтрЗаменить(КонвертXML, "%ТипЗапроса%", "SendRequestRequest"); // Признак отправки запроса
КонвертXML = СтрЗаменить(КонвертXML, "%MessageID%", MessageID);
КонвертXML = СтрЗаменить(КонвертXML, "%ReferenceMessageID%", ReferenceMessageID);
// Обязательно форматируем XML (убираем лишние символы: пробелы, переносы строк)
ФорматироватьXML(КонвертXML);
Указываем идентификатор сообщения (MessageID и ReferenceMessageID), название тега запроса (ТипЗапроса) и сам запрос, дополнительно заключенный в теги <MessagePrimaryContent>. Также не забываем отформатировать полученный XML.
В типе запроса "SendRequestRequest" первое слово "Request" означает запрос с точки зрения бизнес-логики, т.е. мы хотим получить что-то от системы. Второй "Request" несет технический смысл и соответствует SOAP-запросу.
Следующий шаг - непосредственно подписание:
// Создаем менеджер с указанием установленного модуля криптографии
МенеджерКриптографии = Новый МенеджерКриптографии("Crypto-Pro GOST R 34.10-2012 Cryptographic Service Provider", "", 80);
// Получаем сертификат, кодируем по base64 и помещаем значение в %BinarySecurityToken% из шаблона конверта
ОтпечатокСертификата = "1ec8a2794610199helloworldf8459ae4125d2ddbfd0";
Сертификат = МенеджерКриптографии.ПолучитьХранилищеСертификатов().НайтиПоОтпечатку(ПолучитьДвоичныеДанныеИзHEXСтроки(ОтпечатокСертификата));
СертификатКриптографииBase64 = Base64Строка(Сертификат.Выгрузить());
СертификатКриптографииBase64 = СтрЗаменить(СертификатКриптографииBase64, Символы.Таб, "");
СертификатКриптографииBase64 = СтрЗаменить(СертификатКриптографииBase64, Символы.ПС, "");
СертификатКриптографииBase64 = СтрЗаменить(СертификатКриптографииBase64, Символы.ВК, "");
КонвертXML = СтрЗаменить(КонвертXML, "%BinarySecurityToken%", СертификатКриптографииBase64);
// Подключаем компоненту из макета
ПодключитьВнешнююКомпоненту(ПоместитьВоВременноеХранилище(ПолучитьМакет("ExtraCryptoAPI")), "ExtraCryptoAPISymbolicName", ТипВнешнейКомпоненты.Native);
КомпонентаКриптографии = Новый("AddIn.ExtraCryptoAPISymbolicName.ExtraCryptoAPI");
// Приводим текст запроса, заключенный в тегах <MessageData> к единой нормальной форме (канонизируем) по двум алгоритмам (C14N и СМЭВ)
КанонизированныйТекстXMLMessageData = КомпонентаКриптографии.C14N(КонвертXML, "(//. | //@* | //namespace::*)[ancestor-or-self::*[local-name()='MessageData']]");
КанонизированныйТекстXMLMessageData = КомпонентаКриптографии.TransformSMEV(КанонизированныйТекстXMLMessageData);
// Хешируем наш запрос и помещаем значение в %DigestValue%
АтрибутDigestValue = КомпонентаКриптографии.Hash(КанонизированныйТекстXMLMessageData, "1.2.643.7.1.1.2.2", 80);
КонвертXML = СтрЗаменить(КонвертXML, "%DigestValue%", АтрибутDigestValue);
// Канонизируем и подписываем блок <SignedInfo>. Значение помещаем в %SignatureValue%
КанонизированныйТекстXMLSignedInfo = КомпонентаКриптографии.C14N(КонвертXML, "(//. | //@* | //namespace::*)[ancestor-or-self::*[local-name()='SignedInfo']]");
АтрибутSignatureValue = КомпонентаКриптографии.Sign(КанонизированныйТекстXMLSignedInfo, СертификатКриптографииBase64, МенеджерКриптографии.ПарольДоступаКЗакрытомуКлючу);
КонвертXML = СтрЗаменить(КонвертXML, "%SignatureValue%", АтрибутSignatureValue);
Таким образом наш конверт будет выглядеть примерно так:
Информацию об использовании алгоритмов шифрования, хеширования и прочего теоретически можно взять из документации.
4. Отправка запроса и получение ответа
Отправим запрос на тестовый сервер (адрес рабочего "zerno.mcx.gov.ru"):
HTTP = Новый HTTPСоединение("demo-zerno.fors.ru", , , , , 60, Новый ЗащищенноеСоединениеOpenSSL());
// Создаем HTTP запрос
Заголовки = Новый Соответствие;
Заголовки.Вставить("Content-Type", "text/xml;charset=UTF-8");
HTTPЗапрос = Новый HTTPЗапрос("/ws/api/fgiz", Заголовки); // Не fgis, а fgiz :)
HTTPЗапрос.УстановитьТелоИзСтроки(КонвертXML, КодировкаТекста.UTF8);
// Отправляем запрос
HTTPОтвет = HTTP.ОтправитьДляОбработки(HTTPЗапрос).ПолучитьТелоКакСтроку();
Если все сделано правильно, то в переменной HTTPОтвет будет содержаться сообщение об успешном принятии запроса (внимание на слово "accepted"):
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SendRequestResponse
xmlns:ns2="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/faults/1.0.3"
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/types/1.0.3">
<ResponseCode>accepted</ResponseCode>
</SendRequestResponse>
</soap:Body>
</soap:Envelope>
Данный ответ означает, что запрос успешно помещен в очередь. Теперь мы можем попытаться получить ответ (обычно он формируется не сразу, а через некоторое время). Для этого мы должны сформировать аналогичный предыдущему КонвертXML, но с некоторыми отличиями: у него будет пустое поле запроса, а ссылаться он будет на предыдущее сообщение:
// Получаем шаблон конверта
КонвертXML = КонвертXML();
// Формируем новый идентификатор сообщения
MessageID = Строка(Новый УникальныйИдентификатор);
КонвертXML = СтрЗаменить(КонвертXML, "%ТипЗапроса%", "SendResponseRequest"); // Тип запроса теперь другой: получение ответа
КонвертXML = СтрЗаменить(КонвертXML, "%MessagePrimaryContent%", ""); // Оставляем пустым
КонвертXML = СтрЗаменить(КонвертXML, "%MessageID%", MessageID); // Уникальный идентификатор текущего сообщения
КонвертXML = СтрЗаменить(КонвертXML, "%ReferenceMessageID%", ReferenceMessageID); // Ссылка на предыдущее сообщение
КонвертXML = СтрЗаменить(КонвертXML, "%BinarySecurityToken%", СертификатКриптографииBase64); // Сертификат не меняется
ФорматироватьXML(КонвертXML);
// Подписываем аналогично
КанонизированныйТекстXMLMessageData = КомпонентаКриптографии.C14N(КонвертXML, "(//. | //@* | //namespace::*)[ancestor-or-self::*[local-name()='MessageData']]");
КанонизированныйТекстXMLMessageData = КомпонентаКриптографии.TransformSMEV(КанонизированныйТекстXMLMessageData);
АтрибутDigestValue = КомпонентаКриптографии.Hash(КанонизированныйТекстXMLMessageData, "1.2.643.7.1.1.2.2", 80);
КонвертXML = СтрЗаменить(КонвертXML, "%DigestValue%", АтрибутDigestValue);
КанонизированныйТекстXMLSignedInfo = КомпонентаКриптографии.C14N(КонвертXML, "(//. | //@* | //namespace::*)[ancestor-or-self::*[local-name()='SignedInfo']]");
АтрибутSignatureValue = КомпонентаКриптографии.Sign(КанонизированныйТекстXMLSignedInfo, СертификатКриптографииBase64, МенеджерКриптографии.ПарольДоступаКЗакрытомуКлючу);
КонвертXML = СтрЗаменить(КонвертXML, "%SignatureValue%", АтрибутSignatureValue);
HTTPЗапрос.УстановитьТелоИзСтроки(КонвертXML, КодировкаТекста.UTF8);
// Пытаемся получить ответ
HTTPОтвет = HTTP.ОтправитьДляОбработки(HTTPЗапрос).ПолучитьТелоКакСтроку();
Если система успела отработать наш запрос, то в результате получим искомый справочник "ОКПД 2":
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SendResponseResponse
xmlns:ns2="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/faults/1.0.3"
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/types/1.0.3">
<ResponseCode>success</ResponseCode>
<MessageData>
<MessageID>3da6904b-f971-11ed-bb9c-56e09cf3b2cb</MessageID>
<ReferenceMessageID>e9faca53-f4ed-4792-84e1-d6b7650d951d</ReferenceMessageID>
<MessagePrimaryContent>
<dicts:Response
xmlns:dict="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/dictionary/1.0.3"
xmlns:dicts="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/dictionaries/1.0.3" hasMore="false">
<dicts:Items>
<dict:OKPD2 code="01" isGrain="false" isProduct="false" name="Продукция и услуги сельского хозяйства и охоты" startDate="2022-09-02T00:00:00"/>
...Тут много элементов...
<dict:OKPD2 code="10.62.11.169" isGrain="false" isProduct="true" name="Глютен прочий" startDate="2022-09-02T00:00:00"/>
</dicts:Items>
</dicts:Response>
</MessagePrimaryContent>
</MessageData>
</SendResponseResponse>
</soap:Body>
</soap:Envelope>
Обработаем ответ через фабрику XDTO:
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(HTTPОтвет);
ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML);
MessagePrimaryContent = ОбъектXDTO.Body.SendResponseResponse.MessageData.MessagePrimaryContent;
Сообщить(MessagePrimaryContent.Response.Items.OKPD2[0].name); // Выведет "Продукция и услуги сельского хозяйства и охоты"
Создание партии
Напоследок для примера попробуем создать какую-нибудь партию зерна из остатков. Это делается аналогично, только текст запроса будет другим.
Добавляем в фабрику дополнительные схемы для работы с партиями:
Формируем запрос:
ОбъектRequest = Фабрика.Создать(Фабрика.Тип("urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/lots/1.0.3",
"RequestCreateLotType"));
ОбъектLotFromResidues = Фабрика.Создать(Фабрика.Тип("urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3",
"CreateLotFromResiduesType"));
ОбъектLotFromResidues.amount = 1000; // Масса, кг
ОбъектRequest.LotFromResidues = ОбъектLotFromResidues;
ОбъектGrainProduct = Фабрика.Создать(Фабрика.Тип("urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/common/1.0.3",
"GrainProductType"));
ОбъектGrainProduct.OKPD2 = "01.11.49.110"; // Код ОКПД 2
ОбъектRequest.Crop = ОбъектGrainProduct;
ОбъектRequest.Target = 1; // Цель использования: "Пищевые"
ОбъектRequest.Purpose = 1; // Назначение: "Хранение и (или) обработка"
ОбъектRequest.HarvestYear = 2022; // Год урожая
ОбъектAddressRF = Фабрика.Создать(Фабрика.Тип("urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/organizations/1.0.3",
"AddressRFType"));
ОбъектAddressRF.Address = "Москва, Орликов переулок";
ОбъектRequest.StoragePlace = ОбъектAddressRF; // Местоположение
ОбъектListValueQualityIndicator = Фабрика.Создать(Фабрика.Тип("urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/common/1.0.3",
"ListValueQualityIndicatorType"));
ОбъектRequest.ListValueQualityIndicator = ОбъектListValueQualityIndicator; // Потребительские свойства можем не заполнять
МойXML = Новый ЗаписьXML;
МойXML.УстановитьСтроку(Новый ПараметрыЗаписиXML("UTF-8", "1.0", Истина));
Фабрика.ЗаписатьXML(МойXML, ОбъектRequest, "RequestCreateLot");
Request = МойXML.Закрыть();
В итоге получаем следующий текст запроса:
<RequestCreateLot
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/lots/1.0.3"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<LotFromResidues
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3" amount="1000"/>
<Crop
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3"
xmlns:d2p1="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/common/1.0.3">
<d2p1:OKPD2>01.11.49.110</d2p1:OKPD2>
</Crop>
<Target
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3">1
</Target>
<Purpose
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3">1
</Purpose>
<HarvestYear
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3">2022
</HarvestYear>
<StoragePlace
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3"
xmlns:d2p1="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/organizations/1.0.3">
<d2p1:Address>Москва, Орликов переулок</d2p1:Address>
</StoragePlace>
<ListValueQualityIndicator
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3"
xmlns:d2p1="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/common/1.0.3"/>
</RequestCreateLot>
Подписываем его, заворачиваем в конверт и, если все прошло успешно, получаем ответ:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SendResponseResponse
xmlns:ns2="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/faults/1.0.3"
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/types/1.0.3">
<ResponseCode>success</ResponseCode>
<MessageData>
<MessageID>2a89b8c0-f972-11ed-bb9c-56e09cf3b2cb</MessageID>
<ReferenceMessageID>f77cdb09-2a90-4f8c-8af6-7c0c12f62642</ReferenceMessageID>
<MessagePrimaryContent>
<ns4:ResponseCreateLot
xmlns:ns6="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/types/1.0.3"
xmlns:ns5="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/faults/1.0.3"
xmlns:ns3="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/common/1.0.3"
xmlns:ns2="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/organizations/1.0.3"
xmlns="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/lots/1.0.3"
xmlns:ns4="urn://x-artefacts-mcx-gov-ru/fgiz-zerno/api/ws/lots/1.0.3" amountAvailable="1000" amountOriginal="1000" dateRegistration="2023-05-23Z" id="50999" lastModified="2023-05-23T17:00:25.312Z" number="011149110/23/0091" status="SUBSCRIBED">
<LotFromResidues amount="1000"/>
<Crop>
<ns3:OKPD2>01.11.49.110</ns3:OKPD2>
</Crop>
<Target>1</Target>
<Purpose>1</Purpose>
<HarvestYear>2022</HarvestYear>
<StoragePlace>
<ns2:Address>Москва, Орликов переулок</ns2:Address>
</StoragePlace>
<ListValueQualityIndicator/>
<Owner>
<ns2:LegalEntity>
<ns2:Name>ООО «Ромашка»</ns2:Name>
<ns2:ShortName>ООО «Ромашка»</ns2:ShortName>
<ns2:INN>2723092376</ns2:INN>
<ns2:OGRN>1072723048896</ns2:OGRN>
<ns2:KPP>272301501</ns2:KPP>
<ns2:OPF>12100</ns2:OPF>
<ns2:Address>
<ns2:PostalCode>385202</ns2:PostalCode>
<ns2:Address>Адыгея Респ, г Адыгейск, ул Гагарина, д. 120</ns2:Address>
</ns2:Address>
</ns2:LegalEntity>
</Owner>
</ns4:ResponseCreateLot>
</MessagePrimaryContent>
</MessageData>
</SendResponseResponse>
</soap:Body>
</soap:Envelope>
Отлично! Партия была успешно создана. Далее ее можно куда-нибудь послать (оформить СДИЗ - сопроводительный документ), разделить, аннулировать и т.д.
На данный момент имеется ограничение на частоту запросов: 14 в минуту. Если превысить, отправитель будет заблокирован на несколько минут.
P.S.: После получения ответа от сервера подразумевается, исходя из официальной документации, что нужно отправлять третий запрос, AckRequest, в качестве подтверждения ответа. В статье этот момент пропущен, т.к. все работает и без него, но имейте ввиду.