TL;DR: Исчерпывающий гайд по REST API в 1С — от теории до production deployment. Полный цикл разработки: архитектура (OData vs HTTP-сервисы), проектирование, безопасность (JWT, API keys), обработка ошибок, ретраи, валидация JSON, мониторинг, производительность, версионирование и лучшие практики для PROD. 15000+ слов практических примеров.
Современные интеграции требуют предсказуемых, безопасных и масштабируемых API. Платформа 1С 8.3.20+ предоставляет два подхода: автоматический OData REST слой и полностью кастомные HTTP-сервисы. Это исчерпывающее руководство основано на реальном опыте внедрений в enterprise-среде и покрывает весь жизненный цикл — от архитектурных решений до production deployment. Также см. сравнение методов интеграции 1С и раздел услуг.
Теоретические основы REST в контексте 1С
REST (Representational State Transfer) — архитектурный стиль, основанный на принципах HTTP-протокола. В 1С поддержка REST реализована через два основных механизма: автоматический интерфейс OData и собственные HTTP-сервисы.
Ключевые принципы REST в 1С:
- Единообразный интерфейс — ресурсы идентифицируются URL-адресами
- Stateless — сервер не хранит состояние клиента между запросами
- Кэшируемость — поддержка HTTP-кэширования для оптимизации
- Клиент-серверная архитектура с четким разделением ответственности
- Слоистая система — возможность добавления промежуточных слоев (прокси, балансировщики)
- Единообразие кода по требованию (опционально)
OData vs HTTP-сервисы: когда что использовать
Критерий | OData | HTTP-сервисы |
---|---|---|
Скорость разработки | ✅ Автоматическая генерация | ⚠️ Ручная разработка |
Гибкость API | ⚠️ Ограниченная | ✅ Полный контроль |
Производительность | ⚠️ Может быть избыточной | ✅ Оптимизируется под задачи |
Версионирование | ❌ Ограниченное | ✅ Полная поддержка |
Безопасность | ⚠️ Базовая | ✅ Кастомная логика |
Платформа 1С 8.3.20+ автоматически генерирует REST-интерфейс на базе OData 3.0, поддерживая форматы Atom/XML и JSON. Для более гибких решений доступно создание собственных HTTP-сервисов.
💡 Практический совет: Начните с OData для быстрого прототипирования, затем переходите к HTTP-сервисам для production-решений с высокими требованиями к производительности и безопасности.
Объектная модель HTTP в 1С
HTTPСоединение — основа клиентских запросов
Объект HTTPСоединение обеспечивает взаимодействие с внешними REST API. Его ключевые методы соответствуют HTTP-глаголам:
// Создание защищенного соединения с полной настройкой
SSL = Новый ЗащищенноеСоединениеOpenSSL();
Соединение = Новый HTTPСоединение(
"api.cbr.ru", // Сервер
443, // Порт (443 для HTTPS)
, // Пользователь
, // Пароль
, // Прокси
180, // Таймаут (секунды)
SSL // SSL соединение
);
Методы HTTPСоединение:
Получить()
— выполняет GET-запросыОтправитьДляОбработки()
— выполняет POST-запросыЗаписать()
— выполняет PUT-запросыУдалить()
— выполняет DELETE-запросы
HTTPЗапрос и HTTPОтвет — работа с данными
Функция ВыполнитьGETЗапрос(Соединение, АдресРесурса, Заголовки = Неопределено)
Попытка
Запрос = Новый HTTPЗапрос(АдресРесурса);
// Установка заголовков
Если Заголовки <> Неопределено Тогда
Для Каждого Заголовок Из Заголовки Цикл
Запрос.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);
КонецЦикла;
КонецЕсли;
// Установка таймаута для конкретного запроса
Запрос.УстановитьТаймаут(30); // 30 секунд
Ответ = Соединение.Получить(Запрос);
// Детальная проверка кода ответа
Если Ответ.КодСостояния >= 200 И Ответ.КодСостояния < 300 Тогда
Возврат Ответ.ПолучитьТелоКакСтроку("UTF-8");
ИначеЕсли Ответ.КодСостояния = 404 Тогда
ВызватьИсключение "Ресурс не найден";
ИначеЕсли Ответ.КодСостояния = 401 Тогда
ВызватьИсключение "Требуется аутентификация";
ИначеЕсли Ответ.КодСостояния = 403 Тогда
ВызватьИсключение "Доступ запрещен";
ИначеЕсли Ответ.КодСостояния >= 500 Тогда
ВызватьИсключение СтрШаблон("Ошибка сервера: %1", Ответ.КодСостояния);
Иначе
ВызватьИсключение СтрШаблон("Ошибка HTTP %1: %2",
Ответ.КодСостояния,
Ответ.СтатусТекст);
КонецЕсли;
Исключение
ТекстОшибки = СтрШаблон("Ошибка при выполнении GET-запроса к %1: %2",
АдресРесурса,
ОписаниеОшибки());
ЗаписьЖурналаРегистрации("HTTP.Клиент",
УровеньЖурналаРегистрации.Ошибка,,,
ТекстОшибки);
ВызватьИсключение ТекстОшибки;
КонецПопытки;
КонецФункции
Универсальный HTTP-клиент с расширенными возможностями
Функция ВыполнитьHTTPЗапрос(Знач Настройки) Экспорт
// Настройки: Сервер, Порт, Метод, Путь, Тело, Заголовки, Таймаут, SSL
Попытка
// Создание соединения
Если Настройки.Свойство("SSL") И Настройки.SSL Тогда
SSL = Новый ЗащищенноеСоединениеOpenSSL();
Соединение = Новый HTTPСоединение(
Настройки.Сервер,
Настройки.Получить("Порт", 443),
Настройки.Получить("Пользователь"),
Настройки.Получить("Пароль"),
Настройки.Получить("Прокси"),
Настройки.Получить("Таймаут", 60),
SSL
);
Иначе
Соединение = Новый HTTPСоединение(
Настройки.Сервер,
Настройки.Получить("Порт", 80),
Настройки.Получить("Пользователь"),
Настройки.Получить("Пароль"),
Настройки.Получить("Прокси"),
Настройки.Получить("Таймаут", 60)
);
КонецЕсли;
// Создание запроса
Запрос = Новый HTTPЗапрос(Настройки.Путь);
// Установка заголовков по умолчанию
Запрос.Заголовки.Вставить("User-Agent", "1C Enterprise 8.3 REST Client");
Запрос.Заголовки.Вставить("Accept", "application/json");
Запрос.Заголовки.Вставить("Accept-Charset", "utf-8");
// Дополнительные заголовки
Если Настройки.Свойство("Заголовки") Тогда
Для Каждого Заголовок Из Настройки.Заголовки Цикл
Запрос.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);
КонецЦикла;
КонецЕсли;
// Установка тела для POST/PUT
Если Настройки.Свойство("Тело") И НЕ ПустаяСтрока(Настройки.Тело) Тогда
Запрос.УстановитьТелоИзСтроки(Настройки.Тело, "UTF-8");
Если НЕ Запрос.Заголовки.Свойство("Content-Type") Тогда
Запрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
КонецЕсли;
КонецЕсли;
// Выполнение запроса по методу
Метод = ВРег(Настройки.Получить("Метод", "GET"));
Если Метод = "GET" Тогда
Ответ = Соединение.Получить(Запрос);
ИначеЕсли Метод = "POST" Тогда
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
ИначеЕсли Метод = "PUT" Тогда
Ответ = Соединение.Записать(Запрос);
ИначеЕсли Метод = "DELETE" Тогда
Ответ = Соединение.Удалить(Запрос);
Иначе
ВызватьИсключение СтрШаблон("Неподдерживаемый HTTP-метод: %1", Метод);
КонецЕсли;
// Формирование результата
Результат = Новый Структура;
Результат.Вставить("КодСостояния", Ответ.КодСостояния);
Результат.Вставить("СтатусТекст", Ответ.СтатусТекст);
Результат.Вставить("Заголовки", Ответ.Заголовки);
Результат.Вставить("Тело", Ответ.ПолучитьТелоКакСтроку("UTF-8"));
// Попытка десериализации JSON
Если СтрНачинаетсяС(Ответ.Заголовки.Получить("Content-Type"), "application/json") Тогда
Попытка
Результат.Вставить("JSON", ДесериализоватьИзJSON(Результат.Тело));
Исключение
// JSON невалидный, оставляем как текст
КонецПопытки;
КонецЕсли;
Возврат Результат;
Исключение
ВызватьИсключение СтрШаблон("Ошибка HTTP-запроса %1 %2: %3",
Настройки.Получить("Метод", "GET"),
Настройки.Сервер + Настройки.Путь,
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Практический пример: интеграция с ЦБ РФ
Создадим универсальное решение для получения курсов валют от Центрального банка России с полной обработкой ошибок и кэшированием:
Функция ПолучитьКурсыВалютЦБРФ(НаДату = Неопределено) Экспорт
// Используем текущую дату, если не указана другая
Если НаДату = Неопределено Тогда
НаДату = ТекущаяДата();
КонецЕсли;
СтрокаДаты = Формат(НаДату, "ДФ=dd/MM/yyyy");
АдресЗапроса = СтрШаблон("/scripts/XML_daily.asp?date_req=%1", СтрокаДаты);
Попытка
// Создание соединения
SSL = Новый ЗащищенноеСоединениеOpenSSL();
Соединение = Новый HTTPСоединение("www.cbr.ru", 443,,, , 30, SSL);
// Выполнение запроса
Запрос = Новый HTTPЗапрос(АдресЗапроса);
Запрос.Заголовки.Вставить("User-Agent", "1C Enterprise 8.3");
Запрос.Заголовки.Вставить("Accept", "text/xml, application/xml");
Ответ = Соединение.Получить(Запрос);
Если Ответ.КодСостояния <> 200 Тогда
ВызватьИсключение СтрШаблон("ЦБ РФ вернул код %1: %2",
Ответ.КодСостояния, Ответ.СтатусТекст);
КонецЕсли;
// Парсинг XML ответа
ТекстXML = Ответ.ПолучитьТелоКакСтроку("Windows-1251");
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(ТекстXML);
ДокументXML = ПостроительDOM.ПрочитатьДокумент(ЧтениеXML);
// Проверяем корневой элемент
КорневойЭлемент = ДокументXML.ДокументЭлемент;
Если КорневойЭлемент.ИмяУзла <> "ValCurs" Тогда
ВызватьИсключение "Некорректный формат ответа от ЦБ РФ";
КонецЕсли;
Курсы = Новый Массив;
УзлыВалют = ДокументXML.НайтиУзлыПоИмени("Valute");
Для Каждого УзелВалюты Из УзлыВалют Цикл
КурсВалюты = Новый Структура;
КурсВалюты.Вставить("Код", УзелВалюты.АтрибутыЭлемента.Получить("ID"));
КурсВалюты.Вставить("КодЦифра", УзелВалюты.НайтиДочернийЭлемент("NumCode").ТекстовоеСодержимое);
КурсВалюты.Вставить("КодСимвол", УзелВалюты.НайтиДочернийЭлемент("CharCode").ТекстовоеСодержимое);
КурсВалюты.Вставить("Наименование", УзелВалюты.НайтиДочернийЭлемент("Name").ТекстовоеСодержимое);
НоминалТекст = УзелВалюты.НайтиДочернийЭлемент("Nominal").ТекстовоеСодержимое;
КурсТекст = УзелВалюты.НайтиДочернийЭлемент("Value").ТекстовоеСодержимое;
// Обработка числовых значений с учетом локали
КурсВалюты.Вставить("Номинал", Число(НоминалТекст));
КурсВалюты.Вставить("Курс", Число(СтрЗаменить(КурсТекст, ",", ".")));
КурсВалюты.Вставить("КурсЗаЕдиницу", КурсВалюты.Курс / КурсВалюты.Номинал);
КурсВалюты.Вставить("ДатаКурса", НаДату);
Курсы.Добавить(КурсВалюты);
КонецЦикла;
// Логирование успешного получения
ЗаписьЖурналаРегистрации("ИнтеграцияЦБРФ",
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон("Получено курсов валют: %1 на дату %2",
Курсы.Количество(), Формат(НаДату, "ДЛФ=D")));
Возврат Курсы;
Исключение
ТекстОшибки = СтрШаблон("Не удалось получить курсы валют на %1: %2",
Формат(НаДату, "ДЛФ=D"),
ОписаниеОшибки());
ЗаписьЖурналаРегистрации("ИнтеграцияЦБРФ",
УровеньЖурналаРегистрации.Ошибка,,,
ТекстОшибки);
ВызватьИсключение ТекстОшибки;
КонецПопытки;
КонецФункции
Кэширование курсов валют
Для оптимизации добавим кэширование с автоматической инвалидацией:
Функция ПолучитьКурсыВалютСКэшем(НаДату = Неопределено) Экспорт
Если НаДату = Неопределено Тогда
НаДату = ТекущаяДата();
КонецЕсли;
// Проверяем кэш
КлючКэша = СтрШаблон("КурсыЦБРФ_%1", Формат(НаДату, "ДФ=yyyyMMdd"));
Если ХранилищеОбщихНастроек.Загрузить("КэшКурсовВалют", КлючКэша) <> Неопределено Тогда
КэшированныеДанные = ХранилищеОбщихНастроек.Загрузить("КэшКурсовВалют", КлючКэша);
// Проверяем актуальность кэша (не старше 1 часа для текущей даты)
Если НаДату = ТекущаяДата() Тогда
ВремяЖизниКэша = 3600; // 1 час в секундах
Иначе
ВремяЖизниКэша = 86400; // 24 часа для исторических данных
КонецЕсли;
Если ТекущаяУниверсальнаяДата() - КэшированныеДанные.ВремяСоздания < ВремяЖизниКэша Тогда
ЗаписьЖурналаРегистрации("КэшКурсовВалют",
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон("Использован кэш для даты %1", Формат(НаДату, "ДЛФ=D")));
Возврат КэшированныеДанные.Курсы;
КонецЕсли;
КонецЕсли;
// Получаем свежие данные
Курсы = ПолучитьКурсыВалютЦБРФ(НаДату);
// Сохраняем в кэш
ДанныеДляКэша = Новый Структура;
ДанныеДляКэша.Вставить("Курсы", Курсы);
ДанныеДляКэша.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
ХранилищеОбщихНастроек.Сохранить("КэшКурсовВалют", КлючКэша, ДанныеДляКэша);
Возврат Курсы;
КонецФункции
Фильтрация и поиск валют
// Получение курса конкретной валюты
Функция ПолучитьКурсВалюты(КодВалюты, НаДату = Неопределено) Экспорт
Курсы = ПолучитьКурсыВалютСКэшем(НаДату);
Для Каждого КурсВалюты Из Курсы Цикл
Если ВРег(КурсВалюты.КодСимвол) = ВРег(КодВалюты)
ИЛИ КурсВалюты.КодЦифра = КодВалюты Тогда
Возврат КурсВалюты;
КонецЕсли;
КонецЦикла;
ВызватьИсключение СтрШаблон("Валюта с кодом %1 не найдена", КодВалюты);
КонецФункции
// Конвертация валют
Функция КонвертироватьВалюту(Сумма, ВалютаИз, ВалютаВ, НаДату = Неопределено) Экспорт
Если ВРег(ВалютаИз) = ВРег(ВалютаВ) Тогда
Возврат Сумма; // Одинаковые валюты
КонецЕсли;
КурсИз = 1; // RUB по умолчанию
КурсВ = 1;
Если ВРег(ВалютаИз) <> "RUB" Тогда
КурсВалютыИз = ПолучитьКурсВалюты(ВалютаИз, НаДату);
КурсИз = КурсВалютыИз.КурсЗаЕдиницу;
КонецЕсли;
Если ВРег(ВалютаВ) <> "RUB" Тогда
КурсВалютыВ = ПолучитьКурсВалюты(ВалютаВ, НаДату);
КурсВ = КурсВалютыВ.КурсЗаЕдиницу;
КонецЕсли;
// Сначала конвертируем в рубли, потом в целевую валюту
СуммаВРублях = Сумма * КурсИз;
РезультатКонвертации = СуммаВРублях / КурсВ;
Возврат Окр(РезультатКонвертации, 2);
КонецФункции
Создание собственных HTTP-сервисов
Структура HTTP-сервиса
HTTP-сервис в 1С состоит из объекта конфигурации и модуля с обработчиками запросов. URL формируется по шаблону:
http://host/base/hs/корневойURL/относительныйURL
Создание ресурса "Клиенты" - полная реализация
Модуль HTTP-сервиса "КлиентыAPI"
// Получение списка клиентов с продвинутой фильтрацией
Функция КлиентыGET(Запрос)
Попытка
// Логирование входящего запроса
ЛогироватьВходящийЗапрос(Запрос);
СтруктураОтвета = Новый Структура;
// Получение параметров пагинации
Лимит = Мин(Число(Запрос.ПараметрыЗапроса.Получить("limit")), 100); // Максимум 100
Если Лимит = 0 Тогда Лимит = 20; КонецЕсли; // По умолчанию 20
Смещение = Число(Запрос.ПараметрыЗапроса.Получить("offset"));
// Параметры фильтрации
Поиск = Запрос.ПараметрыЗапроса.Получить("search");
ТипКлиента = Запрос.ПараметрыЗапроса.Получить("type"); // individual/legal
Город = Запрос.ПараметрыЗапроса.Получить("city");
Активные = Запрос.ПараметрыЗапроса.Получить("active"); // true/false
// Параметры сортировки
СортировкаПоле = Запрос.ПараметрыЗапроса.Получить("sort");
СортировкаНаправление = ВРег(Запрос.ПараметрыЗапроса.Получить("order")) = "DESC";
// Формирование запроса к базе с динамическими условиями
ТекстЗапроса =
"ВЫБРАТЬ
| Клиенты.Ссылка КАК ID,
| Клиенты.Наименование,
| Клиенты.ИНН,
| Клиенты.КПП,
| Клиенты.Телефон,
| Клиенты.АдресЭлектроннойПочты КАК Email,
| Клиенты.ЮрФизЛицо,
| Клиенты.Город,
| Клиенты.ДатаРегистрации,
| НЕ Клиенты.ПометкаУдаления КАК Активен
|ИЗ
| Справочник.Контрагенты КАК Клиенты
|ГДЕ
| НЕ Клиенты.ПометкаУдаления";
// Динамическое добавление условий
Запрос = Новый Запрос;
Если НЕ ПустаяСтрока(Поиск) Тогда
ТекстЗапроса = ТекстЗапроса + "
| И (Клиенты.Наименование ПОДОБНО &Поиск
| ИЛИ Клиенты.ИНН ПОДОБНО &Поиск
| ИЛИ Клиенты.Телефон ПОДОБНО &Поиск)";
Запрос.УстановитьПараметр("Поиск", "%" + Поиск + "%");
КонецЕсли;
Если НЕ ПустаяСтрока(ТипКлиента) Тогда
Если ВРег(ТипКлиента) = "INDIVIDUAL" Тогда
ТекстЗапроса = ТекстЗапроса + "
| И Клиенты.ЮрФизЛицо = ЗНАЧЕНИЕ(Перечисление.ЮрФизЛицо.ФизЛицо)";
ИначеЕсли ВРег(ТипКлиента) = "LEGAL" Тогда
ТекстЗапроса = ТекстЗапроса + "
| И Клиенты.ЮрФизЛицо = ЗНАЧЕНИЕ(Перечисление.ЮрФизЛицо.ЮрЛицо)";
КонецЕсли;
КонецЕсли;
Если НЕ ПустаяСтрока(Город) Тогда
ТекстЗапроса = ТекстЗапроса + "
| И Клиенты.Город ПОДОБНО &Город";
Запрос.УстановитьПараметр("Город", "%" + Город + "%");
КонецЕсли;
// Сортировка
ПолеСортировки = "Клиенты.Наименование"; // По умолчанию
Если НЕ ПустаяСтрока(СортировкаПоле) Тогда
Если ВРег(СортировкаПоле) = "NAME" Тогда
ПолеСортировки = "Клиенты.Наименование";
ИначеЕсли ВРег(СортировкаПоле) = "DATE" Тогда
ПолеСортировки = "Клиенты.ДатаРегистрации";
ИначеЕсли ВРег(СортировкаПоле) = "CITY" Тогда
ПолеСортировки = "Клиенты.Город";
КонецЕсли;
КонецЕсли;
НаправлениеСортировки = ?(СортировкаНаправление, "УБЫВ", "ВОЗР");
ТекстЗапроса = ТекстЗапроса + "
|УПОРЯДОЧИТЬ ПО
| " + ПолеСортировки + " " + НаправлениеСортировки;
Запрос.Текст = ТекстЗапроса;
РезультатЗапроса = Запрос.Выполнить();
ОбщееКоличество = РезультатЗапроса.Количество();
Выборка = РезультатЗапроса.Выбрать();
// Пропускаем записи до смещения
Для Счетчик = 1 По Смещение Цикл
Если НЕ Выборка.Следующий() Тогда
Прервать;
КонецЕсли;
КонецЦикла;
МассивКлиентов = Новый Массив;
СчетчикВыбранных = 0;
Пока Выборка.Следующий() И СчетчикВыбранных < Лимит Цикл
Клиент = Новый Структура;
Клиент.Вставить("id", XMLСтрока(Выборка.ID));
Клиент.Вставить("name", Выборка.Наименование);
Клиент.Вставить("inn", Выборка.ИНН);
Клиент.Вставить("kpp", Выборка.КПП);
Клиент.Вставить("phone", Выборка.Телефон);
Клиент.Вставить("email", Выборка.Email);
Клиент.Вставить("type", ?(Выборка.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ФизЛицо, "individual", "legal"));
Клиент.Вставить("city", Выборка.Город);
Клиент.Вставить("registrationDate", XMLСтрока(Выборка.ДатаРегистрации));
Клиент.Вставить("active", Выборка.Активен);
МассивКлиентов.Добавить(Клиент);
СчетчикВыбранных = СчетчикВыбранных + 1;
КонецЦикла;
// Метаданные пагинации
СтруктураОтвета.Вставить("data", МассивКлиентов);
СтруктураОтвета.Вставить("pagination", Новый Структура);
СтруктураОтвета.pagination.Вставить("total", ОбщееКоличество);
СтруктураОтвета.pagination.Вставить("limit", Лимит);
СтруктураОтвета.pagination.Вставить("offset", Смещение);
СтруктураОтвета.pagination.Вставить("hasNext", (Смещение + Лимит) < ОбщееКоличество);
СтруктураОтвета.pagination.Вставить("hasPrev", Смещение > 0);
// Сериализация в JSON
СтрокаJSON = СериализоватьВJSON(СтруктураОтвета);
Ответ = Новый HTTPСервисОтвет(200);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
Ответ.Заголовки.Вставить("Cache-Control", "public, max-age=300"); // 5 минут кэша
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8",
ИспользованиеByteOrderMark.НеИспользовать);
// Логирование успешного выполнения
ОтправитьМетрикиAPI("api_clients_get_success", 1,
Новый Структура("method", "GET"));
Возврат Ответ;
Исключение
Возврат СформироватьОтветОбОшибке(500, "Внутренняя ошибка сервера",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Создание нового клиента с валидацией
Функция КлиентыPOST(Запрос)
Попытка
НачатьТранзакцию();
// Парсинг JSON из тела запроса
ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
Если ПустаяСтрока(ТелоЗапроса) Тогда
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(400, "Bad Request",
"Тело запроса не может быть пустым");
КонецЕсли;
ДанныеКлиента = ДесериализоватьИзJSON(ТелоЗапроса);
// Схема валидации
СхемаВалидации = Новый Структура;
СхемаВалидации.Вставить("ОбязательныеПоля", Новый Массив);
СхемаВалидации.ОбязательныеПоля.Добавить("name");
СхемаВалидации.ОбязательныеПоля.Добавить("type");
СхемаВалидации.Вставить("ТипыПолей", Новый Структура);
СхемаВалидации.ТипыПолей.Вставить("name", "Строка");
СхемаВалидации.ТипыПолей.Вставить("type", "Строка");
СхемаВалидации.ТипыПолей.Вставить("inn", "Строка");
СхемаВалидации.ТипыПолей.Вставить("email", "Строка");
// Валидация
РезультатВалидации = ВалидироватьJSONПоСхеме(ТелоЗапроса, СхемаВалидации);
Если НЕ РезультатВалидации.Валиден Тогда
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(400, "Ошибка валидации",
СтрСоединить(РезультатВалидации.Ошибки, "; "));
КонецЕсли;
// Дополнительная бизнес-валидация
Если ДанныеКлиента.Свойство("type") Тогда
Если НЕ (ВРег(ДанныеКлиента.type) = "INDIVIDUAL" ИЛИ ВРег(ДанныеКлиента.type) = "LEGAL") Тогда
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(400, "Bad Request",
"Поле 'type' должно быть 'individual' или 'legal'");
КонецЕсли;
КонецЕсли;
// Проверка уникальности ИНН
Если ДанныеКлиента.Свойство("inn") И НЕ ПустаяСтрока(ДанныеКлиента.inn) Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Контрагенты.Ссылка
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.ИНН = &ИНН
| И НЕ Контрагенты.ПометкаУдаления";
Запрос.УстановитьПараметр("ИНН", ДанныеКлиента.inn);
Если НЕ Запрос.Выполнить().Пустой() Тогда
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(409, "Conflict",
"Клиент с таким ИНН уже существует");
КонецЕсли;
КонецЕсли;
// Создание нового клиента
НовыйКлиент = Справочники.Контрагенты.СоздатьЭлемент();
НовыйКлиент.Наименование = ДанныеКлиента.name;
Если ВРег(ДанныеКлиента.type) = "INDIVIDUAL" Тогда
НовыйКлиент.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ФизЛицо;
Иначе
НовыйКлиент.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ЮрЛицо;
КонецЕсли;
// Заполнение дополнительных полей
Если ДанныеКлиента.Свойство("inn") Тогда
НовыйКлиент.ИНН = ДанныеКлиента.inn;
КонецЕсли;
Если ДанныеКлиента.Свойство("kpp") Тогда
НовыйКлиент.КПП = ДанныеКлиента.kpp;
КонецЕсли;
Если ДанныеКлиента.Свойство("phone") Тогда
НовыйКлиент.Телефон = ДанныеКлиента.phone;
КонецЕсли;
Если ДанныеКлиента.Свойство("email") Тогда
НовыйКлиент.АдресЭлектроннойПочты = ДанныеКлиента.email;
КонецЕсли;
Если ДанныеКлиента.Свойство("city") Тогда
НовыйКлиент.Город = ДанныеКлиента.city;
КонецЕсли;
НовыйКлиент.ДатаРегистрации = ТекущаяДата();
// Проверка заполнения и запись
НовыйКлиент.ПроверитьЗаполнение();
НовыйКлиент.Записать();
ЗафиксироватьТранзакцию();
// Формирование ответа с данными созданного клиента
СозданныйКлиент = Новый Структура;
СозданныйКлиент.Вставить("id", XMLСтрока(НовыйКлиент.Ссылка));
СозданныйКлиент.Вставить("name", НовыйКлиент.Наименование);
СозданныйКлиент.Вставить("type", ?(НовыйКлиент.ЮрФизЛицо = Перечисления.ЮрФизЛицо.ФизЛицо, "individual", "legal"));
СозданныйКлиент.Вставить("inn", НовыйКлиент.ИНН);
СозданныйКлиент.Вставить("kpp", НовыйКлиент.КПП);
СозданныйКлиент.Вставить("phone", НовыйКлиент.Телефон);
СозданныйКлиент.Вставить("email", НовыйКлиент.АдресЭлектроннойПочты);
СозданныйКлиент.Вставить("city", НовыйКлиент.Город);
СозданныйКлиент.Вставить("registrationDate", XMLСтрока(НовыйКлиент.ДатаРегистрации));
СозданныйКлиент.Вставить("active", Истина);
СтрокаJSON = СериализоватьВJSON(СозданныйКлиент);
Ответ = Новый HTTPСервисОтвет(201); // Created
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
Ответ.Заголовки.Вставить("Location", СтрШаблон("/hs/clients/%1", XMLСтрока(НовыйКлиент.Ссылка)));
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8",
ИспользованиеByteOrderMark.НеИспользовать);
// Метрики
ОтправитьМетрикиAPI("api_clients_post_success", 1);
Возврат Ответ;
Исключение
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(500, "Ошибка при создании клиента",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Обновление клиента (PUT)
Функция КлиентыPUT(Запрос)
Попытка
НачатьТранзакцию();
// Получение ID клиента из URL
ИДКлиента = Запрос.ПараметрыURL.Получить("id");
Если ПустаяСтрока(ИДКлиента) Тогда
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(400, "Bad Request",
"Не указан ID клиента");
КонецЕсли;
// Поиск клиента
СсылкаКлиента = XMLЗначение(Тип("СправочникСсылка.Контрагенты"), ИДКлиента);
Если НЕ ЗначениеЗаполнено(СсылкаКлиента) ИЛИ СсылкаКлиента.ПометкаУдаления Тогда
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(404, "Not Found",
"Клиент не найден");
КонецЕсли;
// Парсинг данных
ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
ДанныеКлиента = ДесериализоватьИзJSON(ТелоЗапроса);
// Открытие объекта для изменения
ОбъектКлиента = СсылкаКлиента.ПолучитьОбъект();
// Обновление полей
Если ДанныеКлиента.Свойство("name") Тогда
ОбъектКлиента.Наименование = ДанныеКлиента.name;
КонецЕсли;
Если ДанныеКлиента.Свойство("inn") Тогда
ОбъектКлиента.ИНН = ДанныеКлиента.inn;
КонецЕсли;
Если ДанныеКлиента.Свойство("phone") Тогда
ОбъектКлиента.Телефон = ДанныеКлиента.phone;
КонецЕсли;
Если ДанныеКлиента.Свойство("email") Тогда
ОбъектКлиента.АдресЭлектроннойПочты = ДанныеКлиента.email;
КонецЕсли;
ОбъектКлиента.Записать();
ЗафиксироватьТранзакцию();
// Возврат обновленных данных
ОбновленныйКлиент = ПолучитьДанныеКлиента(СсылкаКлиента);
СтрокаJSON = СериализоватьВJSON(ОбновленныйКлиент);
Ответ = Новый HTTPСервисОтвет(200);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8",
ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
Исключение
ОтменитьТранзакцию();
Возврат СформироватьОтветОбОшибке(500, "Ошибка при обновлении клиента",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Работа с JSON и сериализацией
Универсальный класс для работы с JSON
// Общий модуль УниверсальныйJSON
Функция СериализоватьВJSON(Данные) Экспорт
Попытка
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
НастройкиСериализации = Новый НастройкиСериализацииJSON;
НастройкиСериализации.ФорматДаты = ФорматДатыJSON.ISO;
НастройкиСериализации.ВариантЗаписиДаты = ВариантЗаписиДатыJSON.Универсальная;
НастройкиСериализации.ПереносСтрок = ПереносСтрокJSON.Нет;
НастройкиСериализации.ПробелыEcmascript = Ложь;
НастройкиСериализации.ЭкранированиеСлешей = ЭкранированиеСлешейJSON.НеЭкранировать;
ЗаписатьJSON(ЗаписьJSON, Данные, НастройкиСериализации);
Результат = ЗаписьJSON.Закрыть();
Возврат Результат;
Исключение
ВызватьИсключение СтрШаблон("Ошибка сериализации в JSON: %1",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Функция ДесериализоватьИзJSON(СтрокаJSON) Экспорт
Попытка
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
НастройкиЧтения = Новый НастройкиЧтенияJSON;
НастройкиЧтения.ЧтениеДатыИзСтроки = ЧтениеДатыИзСтроки.ПоДанным;
НастройкиЧтения.ИменаСвойствВВерхнемРегистре = Ложь;
Результат = ПрочитатьJSON(ЧтениеJSON, НастройкиЧтения);
ЧтениеJSON.Закрыть();
Возврат Результат;
Исключение
ВызватьИсключение СтрШаблон("Ошибка десериализации JSON: %1",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
// Сериализация объекта 1С в JSON с использованием XDTO
Функция СериализоватьОбъект1СВJSON(Объект) Экспорт
Попытка
СериализаторXDTO = Новый СериализаторXDTO(ФабрикаXDTO);
НастройкиСериализации = Новый НастройкиСериализацииJSON;
НастройкиСериализации.ФорматДаты = ФорматДатыJSON.ISO;
НастройкиСериализации.ВариантЗаписиДаты = ВариантЗаписиДатыJSON.Универсальная;
СериализованныйОбъект = СериализаторXDTO.ЗаписатьJSON(Объект,
НастройкиСериализации);
Возврат СериализованныйОбъект;
Исключение
ВызватьИсключение СтрШаблон("Ошибка сериализации объекта 1С: %1",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
// Десериализация объекта 1С из JSON
Функция ДесериализоватьОбъект1СИзJSON(СтрокаJSON, ТипОбъекта) Экспорт
Попытка
СериализаторXDTO = Новый СериализаторXDTO(ФабрикаXDTO);
НастройкиЧтения = Новый НастройкиЧтенияJSON;
НастройкиЧтения.ЧтениеДатыИзСтроки = ЧтениеДатыИзСтроки.ПоДанным;
ВосстановленныйОбъект = СериализаторXDTO.ПрочитатьJSON(
СтрокаJSON, ТипОбъекта, НастройкиЧтения);
Возврат ВосстановленныйОбъект;
Исключение
ВызватьИсключение СтрШаблон("Ошибка десериализации объекта 1С: %1",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Продвинутая валидация JSON
Функция ВалидироватьJSONПоСхеме(СтрокаJSON, СхемаВалидации) Экспорт
СтруктураРезультата = Новый Структура;
СтруктураРезультата.Вставить("Валиден", Ложь);
СтруктураРезультата.Вставить("Ошибки", Новый Массив);
СтруктураРезультата.Вставить("Предупреждения", Новый Массив);
Попытка
ДанныеJSON = ДесериализоватьИзJSON(СтрокаJSON);
// Проверка обязательных полей
Если СхемаВалидации.Свойство("ОбязательныеПоля") Тогда
Для Каждого ОбязательноеПоле Из СхемаВалидации.ОбязательныеПоля Цикл
Если НЕ ДанныеJSON.Свойство(ОбязательноеПоле) Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Отсутствует обязательное поле: %1",
ОбязательноеПоле));
ИначеЕсли ТипЗнч(ДанныеJSON[ОбязательноеПоле]) = Тип("Строка")
И ПустаяСтрока(ДанныеJSON[ОбязательноеПоле]) Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Обязательное поле '%1' не может быть пустым",
ОбязательноеПоле));
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Проверка типов данных
Если СхемаВалидации.Свойство("ТипыПолей") Тогда
Для Каждого ОписаниеТипа Из СхемаВалидации.ТипыПолей Цикл
ИмяПоля = ОписаниеТипа.Ключ;
ОжидаемыйТип = ОписаниеТипа.Значение;
Если ДанныеJSON.Свойство(ИмяПоля) Тогда
ФактическийТип = ТипЗнч(ДанныеJSON[ИмяПоля]);
Если ОжидаемыйТип = "Строка" И ФактическийТип <> Тип("Строка") Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' должно быть строкой", ИмяПоля));
ИначеЕсли ОжидаемыйТип = "Число" И ФактическийТип <> Тип("Число") Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' должно быть числом", ИмяПоля));
ИначеЕсли ОжидаемыйТип = "Булево" И ФактическийТип <> Тип("Булево") Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' должно быть булевым значением", ИмяПоля));
ИначеЕсли ОжидаемыйТип = "Дата" И ФактическийТип <> Тип("Дата") Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' должно быть датой", ИмяПоля));
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Проверка ограничений длины строк
Если СхемаВалидации.Свойство("МаксимальныеДлины") Тогда
Для Каждого ОграничениеДлины Из СхемаВалидации.МаксимальныеДлины Цикл
ИмяПоля = ОграничениеДлины.Ключ;
МаксДлина = ОграничениеДлины.Значение;
Если ДанныеJSON.Свойство(ИмяПоля) И ТипЗнч(ДанныеJSON[ИмяПоля]) = Тип("Строка") Тогда
Если СтрДлина(ДанныеJSON[ИмяПоля]) > МаксДлина Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' превышает максимальную длину %2 символов",
ИмяПоля, МаксДлина));
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Проверка регулярных выражений
Если СхемаВалидации.Свойство("Шаблоны") Тогда
Для Каждого ШаблонПроверки Из СхемаВалидации.Шаблоны Цикл
ИмяПоля = ШаблонПроверки.Ключ;
Шаблон = ШаблонПроверки.Значение;
Если ДанныеJSON.Свойство(ИмяПоля) И ТипЗнч(ДанныеJSON[ИмяПоля]) = Тип("Строка") Тогда
Если НЕ СтрСоответствуетШаблону(ДанныеJSON[ИмяПоля], Шаблон) Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' не соответствует требуемому формату", ИмяПоля));
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Проверка допустимых значений (enum)
Если СхемаВалидации.Свойство("ДопустимыеЗначения") Тогда
Для Каждого ОграничениеЗначений Из СхемаВалидации.ДопустимыеЗначения Цикл
ИмяПоля = ОграничениеЗначений.Ключ;
ДопустимыеЗначения = ОграничениеЗначений.Значение;
Если ДанныеJSON.Свойство(ИмяПоля) Тогда
Если ДопустимыеЗначения.Найти(ДанныеJSON[ИмяПоля]) = Неопределено Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' содержит недопустимое значение. Допустимы: %2",
ИмяПоля, СтрСоединить(ДопустимыеЗначения, ", ")));
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Проверка числовых диапазонов
Если СхемаВалидации.Свойство("ЧисловыеДиапазоны") Тогда
Для Каждого Диапазон Из СхемаВалидации.ЧисловыеДиапазоны Цикл
ИмяПоля = Диапазон.Ключ;
ПараметрыДиапазона = Диапазон.Значение;
Если ДанныеJSON.Свойство(ИмяПоля) И ТипЗнч(ДанныеJSON[ИмяПоля]) = Тип("Число") Тогда
Значение = ДанныеJSON[ИмяПоля];
Если ПараметрыДиапазона.Свойство("Минимум") И Значение < ПараметрыДиапазона.Минимум Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' не может быть меньше %2", ИмяПоля, ПараметрыДиапазона.Минимум));
КонецЕсли;
Если ПараметрыДиапазона.Свойство("Максимум") И Значение > ПараметрыДиапазона.Максимум Тогда
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Поле '%1' не может быть больше %2", ИмяПоля, ПараметрыДиапазона.Максимум));
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
СтруктураРезультата.Валиден = (СтруктураРезультата.Ошибки.Количество() = 0);
Исключение
СтруктураРезультата.Ошибки.Добавить(
СтрШаблон("Ошибка валидации JSON: %1", ОписаниеОшибки()));
КонецПопытки;
Возврат СтруктураРезультата;
КонецФункции
Работа с вложенными объектами и массивами
// Функция для работы с комплексными JSON-структурами
Функция ОбработатьВложенныйJSON(ДанныеJSON, СхемаОбработки) Экспорт
Результат = Новый Структура;
Для Каждого ПравилоОбработки Из СхемаОбработки Цикл
ИмяПоля = ПравилоОбработки.Ключ;
ПараметрыОбработки = ПравилоОбработки.Значение;
Если ДанныеJSON.Свойство(ИмяПоля) Тогда
Значение = ДанныеJSON[ИмяПоля];
Если ПараметрыОбработки.ТипОбработки = "Массив" Тогда
// Обработка массива объектов
МассивРезультатов = Новый Массив;
Для Каждого ЭлементМассива Из Значение Цикл
ОбработанныйЭлемент = ОбработатьВложенныйJSON(
ЭлементМассива, ПараметрыОбработки.СхемаЭлемента);
МассивРезультатов.Добавить(ОбработанныйЭлемент);
КонецЦикла;
Результат.Вставить(ИмяПоля, МассивРезультатов);
ИначеЕсли ПараметрыОбработки.ТипОбработки = "Объект" Тогда
// Рекурсивная обработка вложенного объекта
ВложенныйРезультат = ОбработатьВложенныйJSON(
Значение, ПараметрыОбработки.СхемаОбъекта);
Результат.Вставить(ИмяПоля, ВложенныйРезультат);
ИначеЕсли ПараметрыОбработки.ТипОбработки = "Дата" Тогда
// Преобразование строки в дату
Попытка
ДатаЗначение = XMLЗначение(Тип("Дата"), Значение);
Результат.Вставить(ИмяПоля, ДатаЗначение);
Исключение
Результат.Вставить(ИмяПоля, Неопределено);
КонецПопытки;
ИначеЕсли ПараметрыОбработки.ТипОбработки = "СсылкаНаОбъект" Тогда
// Преобразование UUID в ссылку на объект 1С
Попытка
ТипСсылки = ПараметрыОбработки.ТипСсылки;
СсылкаОбъекта = XMLЗначение(ТипСсылки, Значение);
Результат.Вставить(ИмяПоля, СсылкаОбъекта);
Исключение
Результат.Вставить(ИмяПоля, Неопределено);
КонецПопытки;
Иначе
// Простое копирование значения
Результат.Вставить(ИмяПоля, Значение);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Аутентификация и авторизация
JWT токены в 1С 8.3.21+
С версии 8.3.21.1302 платформа 1С получила нативную поддержку JWT-токенов. Это современный стандарт для API аутентификации:
// Создание JWT токена (модуль HTTP-сервиса)
Функция АутентификацияPOST(Запрос)
Попытка
ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
ДанныеАвторизации = ДесериализоватьИзJSON(ТелоЗапроса);
// Валидация входных данных
Если НЕ ДанныеАвторизации.Свойство("username") ИЛИ
НЕ ДанныеАвторизации.Свойство("password") Тогда
Возврат СформироватьОтветОбОшибке(400, "Отсутствуют учетные данные");
КонецЕсли;
// Проверка учетных данных
Пользователь = ПроверитьУчетныеДанные(
ДанныеАвторизации.username,
ДанныеАвторизации.password
);
Если Пользователь = Неопределено Тогда
Возврат СформироватьОтветОбОшибке(401, "Неверные учетные данные");
КонецЕсли;
// Создание access токена
ТокенДоступа = Новый ТокенДоступа;
ТокенДоступа.Аудитория = "mobile-app";
ТокенДоступа.Издатель = "1c-enterprise-api";
ТокенДоступа.КлючСопоставленияПользователя = XMLСтрока(Пользователь);
// Добавление дополнительных claims
ТокенДоступа.Данные.Вставить("userId", XMLСтрока(Пользователь));
ТокенДоступа.Данные.Вставить("username", ДанныеАвторизации.username);
ТокенДоступа.Данные.Вставить("roles", ПолучитьРолиПользователя(Пользователь));
ТокенДоступа.Данные.Вставить("permissions", ПолучитьПравыПользователя(Пользователь));
// Время жизни токена
ВремяВыдачи = ТекущаяУниверсальнаяДата();
ВремяИстечения = ВремяВыдачи + 3600; // 1 час
ТокенДоступа.ВременныеРамки.Добавить(ВремяВыдачи, ВремяИстечения);
// Подписание токена
КлючПодписи = ПолучитьКлючПодписиJWT(); // Должен быть минимум 256 бит
ТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256,
ПолучитьДвоичныеДанныеИзСтроки(КлючПодписи));
СтрокаТокена = ТокенДоступа.ПолучитьТокен();
// Создание refresh токена
РефрешТокен = СоздатьРефрешТокен(Пользователь);
ОтветТокена = Новый Структура;
ОтветТокена.Вставить("access_token", СтрокаТокена);
ОтветТокена.Вставить("token_type", "Bearer");
ОтветТокена.Вставить("expires_in", 3600);
ОтветТокена.Вставить("refresh_token", РефрешТокен);
ОтветТокена.Вставить("scope", "read write");
// Логирование успешной аутентификации
ЗаписьЖурналаРегистрации("JWT.Аутентификация",
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон("Успешная аутентификация пользователя %1", ДанныеАвторизации.username));
Ответ = Новый HTTPСервисОтвет(200);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.Заголовки.Вставить("Cache-Control", "no-store");
Ответ.Заголовки.Вставить("Pragma", "no-cache");
Ответ.УстановитьТелоИзСтроки(СериализоватьВJSON(ОтветТокена),
"UTF-8", ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
Исключение
Возврат СформироватьОтветОбОшибке(500, "Ошибка аутентификации",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Проверка JWT токена
Функция ПроверитьJWTТокен(Запрос)
ЗаголовокАвторизации = Запрос.Заголовки.Получить("Authorization");
Если ПустаяСтрока(ЗаголовокАвторизации) Тогда
Возврат Неопределено;
КонецЕсли;
Если НЕ СтрНачинаетсяС(ВРег(ЗаголовокАвторизации), "BEARER ") Тогда
Возврат Неопределено;
КонецЕсли;
СтрокаТокена = Сред(ЗаголовокАвторизации, 8);
Попытка
КлючПодписи = ПолучитьКлючПодписиJWT();
ТокенДоступа = Новый ТокенДоступа;
ТокенДоступа.УстановитьТокен(СтрокаТокена,
АлгоритмПодписиТокенаДоступа.HS256,
ПолучитьДвоичныеДанныеИзСтроки(КлючПодписи));
// Проверка валидности токена
Если НЕ ТокенДоступа.Валиден Тогда
ЗаписьЖурналаРегистрации("JWT.Ошибка",
УровеньЖурналаРегистрации.Предупреждение,,,
"Получен невалидный JWT токен");
Возврат Неопределено;
КонецЕсли;
// Проверка времени истечения
ТекущееВремя = ТекущаяУниверсальнаяДата();
ВременныеРамки = ТокенДоступа.ВременныеРамки;
ТокенДействителен = Ложь;
Для Каждого ВременнаяРамка Из ВременныеРамки Цикл
Если ТекущееВремя >= ВременнаяРамка.НачалоДействия И
ТекущееВремя <= ВременнаяРамка.ОкончаниеДействия Тогда
ТокенДействителен = Истина;
Прервать;
КонецЕсли;
КонецЦикла;
Если НЕ ТокенДействителен Тогда
ЗаписьЖурналаРегистрации("JWT.Ошибка",
УровеньЖурналаРегистрации.Предупреждение,,,
"JWT токен истек");
Возврат Неопределено;
КонецЕсли;
// Получение пользователя
КлючПользователя = ТокенДоступа.КлючСопоставленияПользователя;
Пользователь = XMLЗначение(Тип("СправочникСсылка.Пользователи"),
КлючПользователя);
// Проверка активности пользователя
Если НЕ ЗначениеЗаполнено(Пользователь) ИЛИ Пользователь.ПометкаУдаления Тогда
Возврат Неопределено;
КонецЕсли;
// Формирование контекста пользователя
КонтекстПользователя = Новый Структура;
КонтекстПользователя.Вставить("Пользователь", Пользователь);
КонтекстПользователя.Вставить("Роли", ТокенДоступа.Данные.Получить("roles"));
КонтекстПользователя.Вставить("Права", ТокенДоступа.Данные.Получить("permissions"));
КонтекстПользователя.Вставить("ИмяПользователя", ТокенДоступа.Данные.Получить("username"));
Возврат КонтекстПользователя;
Исключение
ЗаписьЖурналаРегистрации("JWT.Ошибка",
УровеньЖурналаРегистрации.Ошибка,,,
СтрШаблон("Ошибка проверки JWT токена: %1", ОписаниеОшибки()));
Возврат Неопределено;
КонецПопытки;
КонецФункции
Обновление токена (Refresh)
Функция ОбновитьТокенPOST(Запрос)
Попытка
ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
ДанныеОбновления = ДесериализоватьИзJSON(ТелоЗапроса);
Если НЕ ДанныеОбновления.Свойство("refresh_token") Тогда
Возврат СформироватьОтветОбОшибке(400, "Отсутствует refresh_token");
КонецЕсли;
РефрешТокен = ДанныеОбновления.refresh_token;
// Проверка действительности refresh токена
Пользователь = ПроверитьРефрешТокен(РефрешТокен);
Если Пользователь = Неопределено Тогда
Возврат СформироватьОтветОбОшибке(401, "Недействительный refresh_token");
КонецЕсли;
// Создание нового access токена
НовыйТокенДоступа = Новый ТокенДоступа;
НовыйТокенДоступа.Аудитория = "mobile-app";
НовыйТокенДоступа.Издатель = "1c-enterprise-api";
НовыйТокенДоступа.КлючСопоставленияПользователя = XMLСтрока(Пользователь);
НовыйТокенДоступа.Данные.Вставить("userId", XMLСтрока(Пользователь));
НовыйТокенДоступа.Данные.Вставить("roles", ПолучитьРолиПользователя(Пользователь));
НовыйТокенДоступа.Данные.Вставить("permissions", ПолучитьПравыПользователя(Пользователь));
ВремяВыдачи = ТекущаяУниверсальнаяДата();
ВремяИстечения = ВремяВыдачи + 3600;
НовыйТокенДоступа.ВременныеРамки.Добавить(ВремяВыдачи, ВремяИстечения);
КлючПодписи = ПолучитьКлючПодписиJWT();
НовыйТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256,
ПолучитьДвоичныеДанныеИзСтроки(КлючПодписи));
СтрокаНовогоТокена = НовыйТокенДоступа.ПолучитьТокен();
// Создание нового refresh токена (ротация токенов)
НовыйРефрешТокен = СоздатьРефрешТокен(Пользователь);
// Инвалидация старого refresh токена
ИнвалидироватьРефрешТокен(РефрешТокен);
ОтветТокена = Новый Структура;
ОтветТокена.Вставить("access_token", СтрокаНовогоТокена);
ОтветТокена.Вставить("token_type", "Bearer");
ОтветТокена.Вставить("expires_in", 3600);
ОтветТокена.Вставить("refresh_token", НовыйРефрешТокен);
Ответ = Новый HTTPСервисОтвет(200);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.УстановитьТелоИзСтроки(СериализоватьВJSON(ОтветТокена),
"UTF-8", ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
Исключение
Возврат СформироватьОтветОбОшибке(500, "Ошибка обновления токена",
ОписаниеОшибки());
КонецПопытки;
КонецФункции
Вспомогательные функции для JWT аутентификации
Приведенные выше примеры используют несколько вспомогательных функций, которые необходимо реализовать отдельно:
// === Управление Refresh токенами ===
Функция СоздатьРефрешТокен(Пользователь)
// Создание уникального refresh токена
РефрешТокен = СтрЗаменить(Строка(Новый УникальныйИдентификатор), "-", "");
// Время жизни refresh токена (30 дней)
ВремяИстечения = ТекущаяУниверсальнаяДата() + 30 * 24 * 3600;
// Сохранение в базе данных
НаборЗаписей = РегистрыСведений.RefreshТокены.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Токен.Установить(РефрешТокен);
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Токен = РефрешТокен;
НоваяЗапись.Пользователь = Пользователь;
НоваяЗапись.ДатаСоздания = ТекущаяУниверсальнаяДата();
НоваяЗапись.ДатаИстечения = ВремяИстечения;
НоваяЗапись.Активен = Истина;
НоваяЗапись.IPАдрес = ПолучитьIPАдресКлиента();
НоваяЗапись.UserAgent = ПолучитьUserAgent();
НаборЗаписей.Записать();
Возврат РефрешТокен;
КонецФункции
Функция ПроверитьРефрешТокен(РефрешТокен)
Попытка
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| RefreshТокены.Пользователь КАК Пользователь,
| RefreshТокены.ДатаИстечения
|ИЗ
| РегистрСведений.RefreshТокены КАК RefreshТокены
|ГДЕ
| RefreshТокены.Токен = &Токен
| И RefreshТокены.Активен = ИСТИНА
| И RefreshТокены.ДатаИстечения > &ТекущаяДата";
Запрос.УстановитьПараметр("Токен", РефрешТокен);
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяУниверсальнаяДата());
Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда
Возврат Неопределено;
КонецЕсли;
Выборка = Результат.Выбрать();
Выборка.Следующий();
Возврат Выборка.Пользователь;
Исключение
ЗаписьЖурналаРегистрации("JWT.РефрешТокен",
УровеньЖурналаРегистрации.Ошибка,,,
"Ошибка проверки refresh токена: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
КонецФункции
Процедура ИнвалидироватьРефрешТокен(РефрешТокен)
Попытка
НаборЗаписей = РегистрыСведений.RefreshТокены.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Токен.Установить(РефрешТокен);
НаборЗаписей.Прочитать();
Для Каждого Запись Из НаборЗаписей Цикл
Запись.Активен = Ложь;
Запись.ДатаИнвалидации = ТекущаяУниверсальнаяДата();
КонецЦикла;
НаборЗаписей.Записать();
Исключение
ЗаписьЖурналаРегистрации("JWT.РефрешТокен",
УровеньЖурналаРегистрации.Ошибка,,,
"Ошибка инвалидации refresh токена: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
// === Вспомогательные функции безопасности ===
Функция ПолучитьКлючПодписиJWT()
// В production среде ключ должен храниться в защищенном месте
// Например, в константе с шифрованием или внешнем хранилище ключей
КлючИзКонстанты = Константы.КлючПодписиJWT.Получить();
Если НЕ ПустаяСтрока(КлючИзКонстанты) Тогда
Возврат КлючИзКонстанты;
КонецЕсли;
// Генерация нового ключа при первом запуске
НовыйКлюч = "";
Для Сч = 1 По 64 Цикл
НовыйКлюч = НовыйКлюч + Символ(Окр(СлучайноеЧисло() * 255));
КонецЦикла;
Константы.КлючПодписиJWT.Установить(НовыйКлюч);
Возврат НовыйКлюч;
КонецФункции
Функция ПолучитьРолиПользователя(Пользователь)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РолиПользователей.Роль.Наименование КАК Роль
|ИЗ
| РегистрСведений.РолиПользователей КАК РолиПользователей
|ГДЕ
| РолиПользователей.Пользователь = &Пользователь
| И РолиПользователей.Активен = ИСТИНА";
Запрос.УстановитьПараметр("Пользователь", Пользователь);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Роли = Новый Массив;
Пока Выборка.Следующий() Цикл
Роли.Добавить(Выборка.Роль);
КонецЦикла;
Возврат Роли;
КонецФункции
Функция ПолучитьПравыПользователя(Пользователь)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПравыПользователей.Право,
| ПравыПользователей.Объект
|ИЗ
| РегистрСведений.ПравыПользователей КАК ПравыПользователей
|ГДЕ
| ПравыПользователей.Пользователь = &Пользователь
| И ПравыПользователей.Разрешено = ИСТИНА";
Запрос.УстановитьПараметр("Пользователь", Пользователь);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Права = Новый Структура;
Пока Выборка.Следующий() Цикл
Если НЕ Права.Свойство(Выборка.Право) Тогда
Права.Вставить(Выборка.Право, Новый Массив);
КонецЕсли;
Права[Выборка.Право].Добавить(Выборка.Объект);
КонецЦикла;
Возврат Права;
КонецФункции
Функция ПолучитьIPАдресКлиента()
// Получение IP адреса из заголовков HTTP запроса
Попытка
ИнформацияОСеансе = ПолучитьИнформациюЭкземпляраИнформационнойБазы().АктивныеСоединения[0];
Возврат ИнформацияОСеансе.АдресСоединения;
Исключение
Возврат "unknown";
КонецПопытки;
КонецФункции
Функция ПолучитьUserAgent()
// В контексте HTTP-сервиса можно получить из заголовков запроса
Попытка
Возврат ТекущийHTTPЗапрос.Заголовки.Получить("User-Agent");
Исключение
Возврат "unknown";
КонецПопытки;
КонецФункции
Общие вспомогательные функции
Для корректной работы всех приведенных примеров необходимы следующие общие функции:
// === Обработка ошибок ===
Функция СформироватьОтветОбОшибке(КодСостояния, Сообщение, ПодробностиОшибки = "")
СтруктураОшибки = Новый Структура;
СтруктураОшибки.Вставить("error", Истина);
СтруктураОшибки.Вставить("message", Сообщение);
СтруктураОшибки.Вставить("timestamp", ТекущаяУниверсальнаяДата());
СтруктураОшибки.Вставить("code", КодСостояния);
Если НЕ ПустаяСтрока(ПодробностиОшибки) Тогда
СтруктураОшибки.Вставить("details", ПодробностиОшибки);
КонецЕсли;
// Логирование ошибки
ЗаписьЖурналаРегистрации("API.Ошибка",
УровеньЖурналаРегистрации.Ошибка,,,
СтрШаблон("HTTP %1: %2. Детали: %3", КодСостояния, Сообщение, ПодробностиОшибки));
Ответ = Новый HTTPСервисОтвет(КодСостояния);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.УстановитьТелоИзСтроки(СериализоватьВJSON(СтруктураОшибки),
"UTF-8", ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
КонецФункции
// === Валидация JSON ===
Функция ВалидироватьJSONПоСхеме(СтрокаJSON, СхемаВалидации) Экспорт
// Простая реализация валидации JSON по схеме
Попытка
ДанныеJSON = ДесериализоватьИзJSON(СтрокаJSON);
РезультатВалидации = Новый Структура;
РезультатВалидации.Вставить("Валидно", Истина);
РезультатВалидации.Вставить("Ошибки", Новый Массив);
// Проверка обязательных полей
Если СхемаВалидации.Свойство("required") Тогда
Для Каждого ОбязательноеПоле Из СхемаВалидации.required Цикл
Если НЕ ДанныеJSON.Свойство(ОбязательноеПоле) Тогда
РезультатВалидации.Валидно = Ложь;
РезультатВалидации.Ошибки.Добавить(
СтрШаблон("Отсутствует обязательное поле: %1", ОбязательноеПоле));
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Проверка типов данных
Если СхемаВалидации.Свойство("properties") Тогда
Для Каждого КлючЗначение Из СхемаВалидации.properties Цикл
ИмяПоля = КлючЗначение.Ключ;
ОписаниеПоля = КлючЗначение.Значение;
Если ДанныеJSON.Свойство(ИмяПоля) Тогда
ЗначениеПоля = ДанныеJSON[ИмяПоля];
Если ОписаниеПоля.Свойство("type") Тогда
ОжидаемыйТип = ОписаниеПоля.type;
ФактическийТип = ОпределитьТипJSON(ЗначениеПоля);
Если ОжидаемыйТип <> ФактическийТип Тогда
РезультатВалидации.Валидно = Ложь;
РезультатВалидации.Ошибки.Добавить(
СтрШаблон("Неверный тип поля %1: ожидался %2, получен %3",
ИмяПоля, ОжидаемыйТип, ФактическийТип));
КонецЕсли;
КонецЕсли;
// Проверка длины строки
Если ОписаниеПоля.Свойство("maxLength") И ТипЗнч(ЗначениеПоля) = Тип("Строка") Тогда
Если СтрДлина(ЗначениеПоля) > ОписаниеПоля.maxLength Тогда
РезультатВалидации.Валидно = Ложь;
РезультатВалидации.Ошибки.Добавить(
СтрШаблон("Поле %1 превышает максимальную длину %2",
ИмяПоля, ОписаниеПоля.maxLength));
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
Возврат РезультатВалидации;
Исключение
РезультатВалидации = Новый Структура;
РезультатВалидации.Вставить("Валидно", Ложь);
РезультатВалидации.Вставить("Ошибки", Новый Массив);
РезультатВалидации.Ошибки.Добавить("Ошибка парсинга JSON: " + ОписаниеОшибки());
Возврат РезультатВалидации;
КонецПопытки;
КонецФункции
Функция ОпределитьТипJSON(Значение)
ТипЗначения = ТипЗнч(Значение);
Если ТипЗначения = Тип("Строка") Тогда
Возврат "string";
ИначеЕсли ТипЗначения = Тип("Число") Тогда
Возврат "number";
ИначеЕсли ТипЗначения = Тип("Булево") Тогда
Возврат "boolean";
ИначеЕсли ТипЗначения = Тип("Массив") Тогда
Возврат "array";
ИначеЕсли ТипЗначения = Тип("Структура") ИЛИ ТипЗначения = Тип("Соответствие") Тогда
Возврат "object";
ИначеЕсли Значение = Неопределено Тогда
Возврат "null";
Иначе
Возврат "unknown";
КонецЕсли;
КонецФункции
// === Аутентификация пользователей ===
Функция ПроверитьПодлинностьПользователя(ИмяПользователя, Пароль)
Попытка
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Пользователи.Ссылка КАК Пользователь,
| Пользователи.ХешПароля,
| Пользователи.Соль,
| Пользователи.Активен,
| Пользователи.КоличествоНеудачныхВходов,
| Пользователи.ВременнаяБлокировка
|ИЗ
| Справочник.Пользователи КАК Пользователи
|ГДЕ
| Пользователи.ИмяПользователя = &ИмяПользователя
| И НЕ Пользователи.ПометкаУдаления";
Запрос.УстановитьПараметр("ИмяПользователя", ИмяПользователя);
Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда
Возврат Неопределено;
КонецЕсли;
Выборка = Результат.Выбрать();
Выборка.Следующий();
// Проверка блокировки
Если НЕ Выборка.Активен ИЛИ Выборка.ВременнаяБлокировка > ТекущаяУниверсальнаяДата() Тогда
Возврат Неопределено;
КонецЕсли;
// Проверка пароля
ХешВведенногоПароля = ХешироватьПароль(Пароль, Выборка.Соль);
Если ХешВведенногоПароля = Выборка.ХешПароля Тогда
// Сброс счетчика неудачных попыток
СбросСчетчикаНеудачныхВходов(Выборка.Пользователь);
Возврат Выборка.Пользователь;
Иначе
// Увеличение счетчика неудачных попыток
УвеличитьСчетчикНеудачныхВходов(Выборка.Пользователь);
Возврат Неопределено;
КонецЕсли;
Исключение
ЗаписьЖурналаРегистрации("API.Аутентификация",
УровеньЖурналаРегистрации.Ошибка,,,
"Ошибка аутентификации: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
КонецФункции
Функция ХешироватьПароль(Пароль, Соль)
// Используем SHA-256 с солью для хеширования пароля
ХешированиеДанных = Новый ХешированиеДанных(ХешФункция.SHA256);
ХешированиеДанных.Добавить(ПолучитьДвоичныеДанныеИзСтроки(Соль + Пароль, "UTF-8"));
Возврат Base64Строка(ХешированиеДанных.ХешСумма);
КонецФункции
Процедура СбросСчетчикаНеудачныхВходов(Пользователь)
НачатьТранзакцию();
Попытка
СправочникОбъект = Пользователь.ПолучитьОбъект();
СправочникОбъект.КоличествоНеудачныхВходов = 0;
СправочникОбъект.ВременнаяБлокировка = Дата(1, 1, 1);
СправочникОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;
КонецПроцедуры
Процедура УвеличитьСчетчикНеудачныхВходов(Пользователь)
НачатьТранзакцию();
Попытка
СправочникОбъект = Пользователь.ПолучитьОбъект();
СправочникОбъект.КоличествоНеудачныхВходов = СправочникОбъект.КоличествоНеудачныхВходов + 1;
// Блокировка после 5 неудачных попыток на 15 минут
Если СправочникОбъект.КоличествоНеудачныхВходов >= 5 Тогда
СправочникОбъект.ВременнаяБлокировка = ТекущаяУниверсальнаяДата() + 15 * 60; // 15 минут
КонецЕсли;
СправочникОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;
КонецПроцедуры
// === Функции кеширования ===
Функция ПолучитьИзКеша(КлючКэша, КатегорияКэша = "Общий")
Попытка
КэшированныеДанные = ХранилищеОбщихНастроек.Загрузить(КатегорияКэша, КлючКэша);
Если КэшированныеДанные = Неопределено Тогда
Возврат Неопределено;
КонецЕсли;
// Проверка времени жизни кэша
Если КэшированныеДанные.Свойство("ВремяИстечения") Тогда
Если ТекущаяУниверсальнаяДата() > КэшированныеДанные.ВремяИстечения Тогда
// Кэш устарел, удаляем его
ХранилищеОбщихНастроек.Удалить(КатегорияКэша, КлючКэша);
Возврат Неопределено;
КонецЕсли;
КонецЕсли;
Возврат КэшированныеДанные.Данные;
Исключение
ЗаписьЖурналаРегистрации("Кэширование",
УровеньЖурналаРегистрации.Ошибка,,,
СтрШаблон("Ошибка получения из кэша: %1", ОписаниеОшибки()));
Возврат Неопределено;
КонецПопытки;
КонецФункции
Процедура ПоместитьВКеш(КлючКэша, Данные, ВремяЖизниВСекундах = 3600, КатегорияКэша = "Общий")
Попытка
КэшированныеДанные = Новый Структура;
КэшированныеДанные.Вставить("Данные", Данные);
КэшированныеДанные.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
КэшированныеДанные.Вставить("ВремяИстечения",
ТекущаяУниверсальнаяДата() + ВремяЖизниВСекундах);
ХранилищеОбщихНастроек.Сохранить(КатегорияКэша, КлючКэша, КэшированныеДанные);
ЗаписьЖурналаРегистрации("Кэширование",
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон("Данные помещены в кэш: %1 (время жизни: %2 сек)",
КлючКэша, ВремяЖизниВСекундах));
Исключение
ЗаписьЖурналаРегистрации("Кэширование",
УровеньЖурналаРегистрации.Ошибка,,,
СтрШаблон("Ошибка помещения в кэш: %1", ОписаниеОшибки()));
КонецПопытки;
КонецПроцедуры
Процедура ОчиститьКеш(КатегорияКэша = "Общий", КлючМаска = "")
Попытки
Если ПустаяСтрока(КлючМаска) Тогда
// Очистка всей категории
ХранилищеОбщихНастроек.Удалить(КатегорияКэша);
Иначе
// Получение всех ключей и удаление по маске
ВсеКлючи = ХранилищеОбщихНастроек.ПолучитьСписок(КатегорияКэша);
Для Каждого КлючЗначение Из ВсеКлючи Цикл
Если СтрНайти(КлючЗначение.Ключ, КлючМаска) > 0 Тогда
ХранилищеОбщихНастроек.Удалить(КатегорияКэша, КлючЗначение.Ключ);
КонецЕсли;
КонецЦикла;
КонецЕсли;
ЗаписьЖурналаРегистрации("Кэширование",
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон("Очищен кэш категории '%1' по маске '%2'", КатегорияКэша, КлючМаска));
Исключение
ЗаписьЖурналаРегистрации("Кэширование",
УровеньЖурналаРегистрации.Ошибка,,,
СтрШаблон("Ошибка очистки кэша: %1", ОписаниеОшибки()));
КонецПопытки;
КонецПроцедуры
Basic Authentication для простых случаев
Функция ПроверитьBasicAuth(Запрос)
ЗаголовокАвторизации = Запрос.Заголовки.Получить("Authorization");
Если ПустаяСтрока(ЗаголовокАвторизации) Тогда
Возврат Неопределено;
КонецЕсли;
Если НЕ СтрНачинаетсяС(ВРег(ЗаголовокАвторизации), "BASIC ") Тогда
Возврат Неопределено;
КонецЕсли;
Попытка
КодированныеДанные = Сред(ЗаголовокАвторизации, 7);
ДвоичныеДанные = Base64Значение(КодированныеДанные);
СтрокаАвторизации = ПолучитьСтрокуИзДвоичныхДанных(ДвоичныеДанные);
ПозицияДвоеточия = СтрНайти(СтрокаАвторизации, ":");
Если ПозицияДвоеточия = 0 Тогда
Возврат Неопределено;
КонецЕсли;
Логин = Лев(СтрокаАвторизации, ПозицияДвоеточия - 1);
Пароль = Сред(СтрокаАвторизации, ПозицияДвоеточия + 1);
// Проверка на пустые значения
Если ПустаяСтрока(Логин) ИЛИ ПустаяСтрока(Пароль) Тогда
Возврат Неопределено;
КонецЕсли;
Возврат ПроверитьУчетныеДанные(Логин, Пароль);
Исключение
ЗаписьЖурналаРегистрации("BasicAuth.Ошибка",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Ошибка декодирования Basic Auth: %1", ОписаниеОшибки()));
Возврат Неопределено;
КонецПопытки;
КонецФункции
API Key аутентификация
Функция ПроверитьAPIKey(Запрос)
// Проверяем заголовок X-API-Key
APIKey = Запрос.Заголовки.Получить("X-API-Key");
// Альтернативно - параметр запроса
Если ПустаяСтрока(APIKey) Тогда
APIKey = Запрос.ПараметрыЗапроса.Получить("api_key");
КонецЕсли;
Если ПустаяСтрока(APIKey) Тогда
Возврат Неопределено;
КонецЕсли;
// Поиск пользователя по API ключу
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Пользователи.Ссылка КАК Пользователь,
| Пользователи.Наименование,
| APIКлючи.Права,
| APIКлючи.ДатаИстечения,
| APIКлючи.КоличествоЗапросов,
| APIКлючи.МаксимумЗапросовВДень
|ИЗ
| РегистрСведений.APIКлючи КАК APIКлючи
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Пользователи КАК Пользователи
| ПО APIКлючи.Пользователь = Пользователи.Ссылка
|ГДЕ
| APIКлючи.Ключ = &APIKey
| И APIКлючи.Активен
| И НЕ Пользователи.ПометкаУдаления
| И (APIКлючи.ДатаИстечения = ДАТАВРЕМЯ(1, 1, 1)
| ИЛИ APIКлючи.ДатаИстечения > &ТекущаяДата)";
Запрос.УстановитьПараметр("APIKey", APIKey);
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
Выборка = Запрос.Выполнить().Выбрать();
Если НЕ Выборка.Следующий() Тогда
ЗаписьЖурналаРегистрации("APIKey.Ошибка",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Недействительный API ключ: %1", Лев(APIKey, 8) + "..."));
Возврат Неопределено;
КонецЕсли;
// Проверка лимитов запросов
Если Выборка.МаксимумЗапросовВДень > 0 И
Выборка.КоличествоЗапросов >= Выборка.МаксимумЗапросовВДень Тогда
ЗаписьЖурналаРегистрации("APIKey.Лимит",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Превышен лимит запросов для API ключа пользователя %1",
Выборка.Наименование));
Возврат "RATE_LIMIT_EXCEEDED";
КонецЕсли;
// Увеличение счетчика запросов
УвеличитьСчетчикЗапросовAPIKey(APIKey);
КонтекстПользователя = Новый Структура;
КонтекстПользователя.Вставить("Пользователь", Выборка.Пользователь);
КонтекстПользователя.Вставить("Права", Выборка.Права);
КонтекстПользователя.Вставить("ТипАутентификации", "APIKey");
Возврат КонтекстПользователя;
КонецФункции
Обработка ошибок и диагностика
Комплексная система обработки HTTP-ошибок
// Общий модуль ОбработкаОшибокHTTP
Функция ВыполнитьHTTPЗапросСПовторами(Соединение, Запрос, МаксимумПовторов = 3) Экспорт
ДанныеОперации = Новый Структура;
ДанныеОперации.Вставить("НачалоВыполнения", ТекущаяУниверсальнаяДата());
ДанныеОперации.Вставить("URLЗапроса", Запрос.АдресРесурса);
ДанныеОперации.Вставить("МетодHTTP", ОпределитьМетодHTTP(Соединение, Запрос));
ДанныеОперации.Вставить("КоличествоПопыток", 0);
Для НомерПопытки = 1 По МаксимумПовторов Цикл
ДанныеОперации.КоличествоПопыток = НомерПопытки;
Попытка
// Определяем метод и выполняем запрос
Если ДанныеОперации.МетодHTTP = "GET" Тогда
Ответ = Соединение.Получить(Запрос);
ИначеЕсли ДанныеОперации.МетодHTTP = "POST" Тогда
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
ИначеЕсли ДанныеОперации.МетодHTTP = "PUT" Тогда
Ответ = Соединение.Записать(Запрос);
ИначеЕсли ДанныеОперации.МетодHTTP = "DELETE" Тогда
Ответ = Соединение.Удалить(Запрос);
КонецЕсли;
// Анализ кода ответа
Если Ответ.КодСостояния >= 200 И Ответ.КодСостояния < 300 Тогда
// Успешный запрос
ДанныеОперации.Вставить("РезультатОперации", "Успех");
ДанныеОперации.Вставить("КодОтвета", Ответ.КодСостояния);
ДанныеОперации.Вставить("ВремяВыполнения",
(ТекущаяУниверсальнаяДата() - ДанныеОперации.НачалоВыполнения) * 1000); // мс
ДанныеОперации.Вставить("РазмерОтвета", СтрДлина(Ответ.ПолучитьТелоКакСтроку()));
ЗаписатьЛогОперации(ДанныеОперации);
Возврат Ответ;
ИначеЕсли Ответ.КодСостояния >= 500 И НомерПопытки < МаксимумПовторов Тогда
// Ошибка сервера - попробуем повторить
ВремяЗадержки = РассчитатьВремяЗадержки(НомерПопытки, Ответ);
ЗаписьЖурналаРегистрации("HTTP.ПовторныйЗапрос",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Повтор %1/%2 через %3 сек. Код: %4, URL: %5",
НомерПопытки, МаксимумПовторов, ВремяЗадержки,
Ответ.КодСостояния, Запрос.АдресРесурса));
Приостановить(ВремяЗадержки * 1000); // миллисекунды
Продолжить;
ИначеЕсли Ответ.КодСостояния >= 400 И Ответ.КодСостояния < 500 Тогда
// Ошибка клиента - повторять нет смысла
ДанныеОперации.Вставить("РезультатОперации", "ОшибкаКлиента");
ДанныеОперации.Вставить("КодОтвета", Ответ.КодСостояния);
ЗаписатьЛогОперации(ДанныеОперации);
ИсключениеHTTP = СформироватьИсключениеHTTP(Ответ);
ВызватьИсключение ИсключениеHTTP;
КонецЕсли;
Исключение
Если НомерПопытки = МаксимумПовторов Тогда
ДанныеОперации.Вставить("РезультатОперации", "ОшибкаСоединения");
ДанныеОперации.Вставить("ТекстОшибки", ОписаниеОшибки());
ЗаписатьЛогОперации(ДанныеОперации);
ВызватьИсключение;
КонецЕсли;
ЗаписьЖурналаРегистрации("HTTP.ОшибкаСоединения",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Попытка %1/%2 для %3: %4",
НомерПопытки, МаксимумПовторов, Запрос.АдресРесурса, ОписаниеОшибки()));
// Экспоненциальная задержка при ошибке соединения
ВремяЗадержки = Степень(2, НомерПопытки - 1); // 1, 2, 4, 8...
Приостановить(ВремяЗадержки * 1000);
КонецПопытки;
КонецЦикла;
КонецФункции
Функция РассчитатьВремяЗадержки(НомерПопытки, Ответ)
// Проверяем заголовок Retry-After
RetryAfter = Ответ.Заголовки.Получить("Retry-After");
Если НЕ ПустаяСтрока(RetryAfter) Тогда
ЧисловоеЗначение = 0;
Попытка
ЧисловоеЗначение = Число(RetryAfter);
Если ЧисловоеЗначение > 0 И ЧисловоеЗначение <= 300 Тогда // Максимум 5 минут
Возврат ЧисловоеЗначение;
КонецЕсли;
Исключение
// Возможно, значение в формате даты HTTP, игнорируем
КонецПопытки;
КонецЕсли;
// Экспоненциальная задержка с джиттером
БазоваяЗадержка = Степень(2, НомерПопытки - 1); // 1, 2, 4, 8...
МаксимальнаяЗадержка = 60; // Не больше минуты
БазоваяЗадержка = Мин(БазоваяЗадержка, МаксимальнаяЗадержка);
// Добавляем случайный джиттер ±25%
Джиттер = Цел(Случайное() * БазоваяЗадержка * 0.5) - БазоваяЗадержка * 0.25;
ИтоговаяЗадержка = БазоваяЗадержка + Джиттер;
Возврат Макс(1, ИтоговаяЗадержка); // Минимум 1 секунда
КонецФункции
Функция СформироватьИсключениеHTTP(Ответ)
ТипОшибки = "";
РекомендацииПользователю = "";
ДетальноеОписание = "";
Если Ответ.КодСостояния = 400 Тогда
ТипОшибки = "Некорректный запрос";
РекомендацииПользователю = "Проверьте правильность передаваемых данных";
ИначеЕсли Ответ.КодСостояния = 401 Тогда
ТипОшибки = "Требуется аутентификация";
РекомендацииПользователю = "Проверьте учетные данные или токен доступа";
ИначеЕсли Ответ.КодСостояния = 403 Тогда
ТипОшибки = "Доступ запрещен";
РекомендацииПользователю = "Недостаточно прав для выполнения операции";
ИначеЕсли Ответ.КодСостояния = 404 Тогда
ТипОшибки = "Ресурс не найден";
РекомендацииПользователю = "Проверьте правильность URL и существование ресурса";
ИначеЕсли Ответ.КодСостояния = 409 Тогда
ТипОшибки = "Конфликт данных";
РекомендацииПользователю = "Ресурс уже существует или изменен другим пользователем";
ИначеЕсли Ответ.КодСостояния = 422 Тогда
ТипОшибки = "Ошибка валидации";
РекомендацииПользователю = "Проверьте корректность формата данных";
ИначеЕсли Ответ.КодСостояния = 429 Тогда
ТипОшибки = "Превышен лимит запросов";
РекомендацииПользователю = "Повторите попытку позже";
// Извлекаем время ожидания из заголовка
RetryAfter = Ответ.Заголовки.Получить("Retry-After");
Если НЕ ПустаяСтрока(RetryAfter) Тогда
РекомендацииПользователю = РекомендацииПользователю +
СтрШаблон(" (рекомендуемое время ожидания: %1 сек)", RetryAfter);
КонецЕсли;
ИначеЕсли Ответ.КодСостояния >= 500 Тогда
ТипОшибки = "Ошибка сервера";
РекомендацииПользователю = "Временная проблема на сервере, повторите попытку позже";
Иначе
ТипОшибки = СтрШаблон("HTTP ошибка %1", Ответ.КодСостояния);
РекомендацииПользователю = "Обратитесь к администратору системы";
КонецЕсли;
// Попытка извлечь детали ошибки из тела ответа
Попытка
ТелоОтвета = Ответ.ПолучитьТелоКакСтроку("UTF-8");
Если НЕ ПустаяСтрока(ТелоОтвета) И СтрДлина(ТелоОтвета) < 1000 Тогда
// Проверяем, является ли ответ JSON
Если СтрНачинаетсяС(Ответ.Заголовки.Получить("Content-Type"), "application/json") Тогда
ДанныеОшибки = ДесериализоватьИзJSON(ТелоОтвета);
Если ДанныеОшибки.Свойство("message") Тогда
ДетальноеОписание = ДанныеОшибки.message;
ИначеЕсли ДанныеОшибки.Свойство("error") Тогда
ДетальноеОписание = ДанныеОшибки.error;
КонецЕсли;
Иначе
ДетальноеОписание = ТелоОтвета;
КонецЕсли;
КонецЕсли;
Исключение
// Игнорируем ошибки парсинга тела ответа
КонецПопытки;
СообщениеОбОшибке = СтрШаблон("%1: %2. %3",
ТипОшибки,
Ответ.СтатусТекст,
РекомендацииПользователю);
Если НЕ ПустаяСтрока(ДетальноеОписание) Тогда
СообщениеОбОшибке = СообщениеОбОшибке + Символы.ПС + "Детали: " + ДетальноеОписание;
КонецЕсли;
Возврат СообщениеОбОшибке;
КонецФункции
Логирование операций и мониторинг
Процедура ЗаписатьЛогОперации(ДанныеОперации) Экспорт
// Подготовка данных для журнала
СтруктураЛога = Новый Структура;
СтруктураЛога.Вставить("timestamp", XMLСтрока(ДанныеОперации.НачалоВыполнения));
СтруктураЛога.Вставить("method", ДанныеОперации.МетодHTTP);
СтруктураЛога.Вставить("url", ДанныеОперации.URLЗапроса);
СтруктураЛога.Вставить("result", ДанныеОперации.РезультатОперации);
СтруктураЛога.Вставить("attempts", ДанныеОперации.КоличествоПопыток);
Если ДанныеОперации.Свойство("КодОтвета") Тогда
СтруктураЛога.Вставить("statusCode", ДанныеОперации.КодОтвета);
КонецЕсли;
Если ДанныеОперации.Свойство("ВремяВыполнения") Тогда
СтруктураЛога.Вставить("durationMs", ДанныеОперации.ВремяВыполнения);
КонецЕсли;
Если ДанныеОперации.Свойство("РазмерОтвета") Тогда
СтруктураЛога.Вставить("responseSize", ДанныеОперации.РазмерОтвета);
КонецЕсли;
Если ДанныеОперации.Свойство("ТекстОшибки") Тогда
СтруктураЛога.Вставить("error", ДанныеОперации.ТекстОшибки);
КонецЕсли;
СтрокаЛога = СериализоватьВJSON(СтруктураЛога);
// Определение уровня логирования
УровеньЛога = УровеньЖурналаРегистрации.Информация;
Если ДанныеОперации.РезультатОперации = "Успех" Тогда
УровеньЛога = УровеньЖурналаРегистрации.Информация;
ИначеЕсли ДанныеОперации.РезультатОперации = "ОшибкаКлиента" Тогда
УровеньЛога = УровеньЖурналаРегистрации.Предупреждение;
Иначе
УровеньЛога = УровеньЖурналаРегистрации.Ошибка;
КонецЕсли;
ЗаписьЖурналаРегистрации("HTTP.Операция", УровеньЛога,,,СтрокаЛога);
// Отправка метрик (неблокирующая)
Попытка
ОтправитьМетрикиHTTP(ДанныеОперации);
Исключение
// Ошибки метрик не должны влиять на основную логику
КонецПопытки;
КонецПроцедуры
Процедура ОтправитьМетрикиHTTP(ДанныеОперации)
// Счетчик запросов по методам
ОтправитьМетрикиAPI("http_requests_total", 1,
Новый Структура("method", ДанныеОперации.МетодHTTP;
"result", ДанныеОперации.РезультатОперации));
// Время выполнения
Если ДанныеОперации.Свойство("ВремяВыполнения") Тогда
ОтправитьМетрикиAPI("http_request_duration_ms", ДанныеОперации.ВремяВыполнения,
Новый Структура("method", ДанныеОперации.МетодHTTP));
КонецЕсли;
// Размер ответа
Если ДанныеОперации.Свойство("РазмерОтвета") Тогда
ОтправитьМетрикиAPI("http_response_size_bytes", ДанныеОперации.РазмерОтвета,
Новый Структура("method", ДанныеОперации.МетодHTTP));
КонецЕсли;
// Количество попыток
ОтправитьМетрикиAPI("http_request_attempts", ДанныеОперации.КоличествоПопыток,
Новый Структура("method", ДанныеОперации.МетодHTTP));
КонецПроцедуры
Отладка HTTP-сервисов
Для эффективной отладки HTTP-сервисов создадим универсальную систему логирования:
// Функция для детального логирования HTTP-запросов (модуль HTTP-сервиса)
Функция ЛогироватьВходящийЗапрос(Запрос) Экспорт
СтруктураЛога = Новый Структура;
СтруктураЛога.Вставить("timestamp", XMLСтрока(ТекущаяУниверсальнаяДата()));
СтруктураЛога.Вставить("method", Запрос.HTTPМетод);
СтруктураЛога.Вставить("url", Запрос.ОтносительныйURL);
СтруктураЛога.Вставить("remoteAddr", Запрос.АдресИсточника);
СтруктураЛога.Вставить("userAgent", Запрос.Заголовки.Получить("User-Agent"));
СтруктураЛога.Вставить("contentType", Запрос.Заголовки.Получить("Content-Type"));
СтруктураЛога.Вставить("contentLength", Число(Запрос.Заголовки.Получить("Content-Length")));
// Логирование параметров запроса
Если Запрос.ПараметрыЗапроса.Количество() > 0 Тогда
СтруктураЛога.Вставить("queryParams", Новый Структура);
Для Каждого Параметр Из Запрос.ПараметрыЗапроса Цикл
СтруктураЛога.queryParams.Вставить(Параметр.Ключ, Параметр.Значение);
КонецЦикла;
КонецЕсли;
// Логирование заголовков (кроме конфиденциальных)
СтруктураЛога.Вставить("headers", Новый Структура);
КонфиденциальныеЗаголовки = Новый Массив;
КонфиденциальныеЗаголовки.Добавить("AUTHORIZATION");
КонфиденциальныеЗаголовки.Добавить("X-API-KEY");
КонфиденциальныеЗаголовки.Добавить("COOKIE");
Для Каждого Заголовок Из Запрос.Заголовки Цикл
Если КонфиденциальныеЗаголовки.Найти(ВРег(Заголовок.Ключ)) <> Неопределено Тогда
СтруктураЛога.headers.Вставить(Заголовок.Ключ, "[СКРЫТО]");
Иначе
СтруктураЛога.headers.Вставить(Заголовок.Ключ, Заголовок.Значение);
КонецЕсли;
КонецЦикла;
// Логирование тела запроса (с ограничениями)
Если Запрос.HTTPМетод = "POST" ИЛИ Запрос.HTTPМетод = "PUT" ИЛИ Запрос.HTTPМетод = "PATCH" Тогда
ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку("UTF-8");
Если СтрДлина(ТелоЗапроса) <= 2000 Тогда // Ограничение размера лога
СтруктураЛога.Вставить("body", ТелоЗапроса);
Иначе
СтруктураЛога.Вставить("body", СтрШаблон("[БОЛЬШОЕ ТЕЛО ЗАПРОСА: %1 символов]",
СтрДлина(ТелоЗапроса)));
КонецЕсли;
КонецЕсли;
СтрокаЛога = СериализоватьВJSON(СтруктураЛога);
ЗаписьЖурналаРегистрации("HTTP.ВходящийЗапрос",
УровеньЖурналаРегистрации.Информация,,,
СтрокаЛога);
КонецФункции
// Логирование исходящих ответов
Функция ЛогироватьИсходящийОтвет(Ответ, ВремяОбработки = 0) Экспорт
СтруктураЛога = Новый Структура;
СтруктураЛога.Вставить("timestamp", XMLСтрока(ТекущаяУниверсальнаяДата()));
СтруктураЛога.Вставить("statusCode", Ответ.КодСостояния);
СтруктураЛога.Вставить("statusText", Ответ.ПолучитьСтатусТекст());
СтруктураЛога.Вставить("processingTimeMs", ВремяОбработки);
// Логирование заголовков ответа
СтруктураЛога.Вставить("responseHeaders", Новый Структура);
Для Каждого Заголовок Из Ответ.Заголовки Цикл
СтруктураЛога.responseHeaders.Вставить(Заголовок.Ключ, Заголовок.Значение);
КонецЦикла;
// Размер тела ответа
РазмерТела = 0;
Попытка
ТелоОтвета = Ответ.ПолучитьТелоКакСтроку("UTF-8");
РазмерТела = СтрДлина(ТелоОтвета);
СтруктураЛога.Вставить("responseSize", РазмерТела);
// Логируем только малые ответы или ошибки
Если РазмерТела <= 1000 ИЛИ Ответ.КодСостояния >= 400 Тогда
СтруктураЛога.Вставить("responseBody", ТелоОтвета);
КонецЕсли;
Исключение
СтруктураЛога.Вставить("responseSize", 0);
КонецПопытки;
СтрокаЛога = СериализоватьВJSON(СтруктураЛога);
УровеньЛога = УровеньЖурналаРегистрации.Информация;
Если Ответ.КодСостояния >= 400 Тогда
УровеньЛога = УровеньЖурналаРегистрации.Предупреждение;
КонецЕсли;
ЗаписьЖурналаРегистрации("HTTP.ИсходящийОтвет", УровеньЛога,,,СтрокаЛога);
КонецФункции
Мониторинг и метрики
Интеграция с системами мониторинга
// Процедура для отправки метрик в Prometheus Push Gateway
Процедура ОтправитьМетрикиAPI(ИмяМетрики, Значение, Теги = Неопределено) Экспорт
Попытка
СтруктураМетрики = Новый Структура;
СтруктураМетрики.Вставить("metric", ИмяМетрики);
СтруктураМетрики.Вставить("value", Значение);
СтруктураМетрики.Вставить("timestamp", ТекущаяУниверсальнаяДатаВМиллисекундах());
СтруктураМетрики.Вставить("instance", ПолучитьИмяКомпьютера());
СтруктураМетрики.Вставить("job", "1c-api");
Если Теги <> Неопределено Тогда
СтруктураМетрики.Вставить("labels", Теги);
КонецЕсли;
// Формирование Prometheus формата
СтрокаМетрики = СформироватьPrometheusМетрику(СтруктураМетрики);
// Отправка в Push Gateway
SSL = Новый ЗащищенноеСоединениеOpenSSL();
Соединение = Новый HTTPСоединение("monitoring.company.ru", 9091,,,,30, SSL);
Запрос = Новый HTTPЗапрос("/metrics/job/1c-api/instance/" + ПолучитьИмяКомпьютера());
Запрос.УстановитьТелоИзСтроки(СтрокаМетрики, "UTF-8");
Запрос.Заголовки.Вставить("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
Если Ответ.КодСостояния <> 200 Тогда
ЗаписьЖурналаРегистрации("МониторингAPI.Ошибка",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Push Gateway вернул код %1 для метрики %2",
Ответ.КодСостояния, ИмяМетрики));
КонецЕсли;
Исключение
// Ошибки мониторинга не должны влиять на основную логику
ЗаписьЖурналаРегистрации("МониторингAPI.Исключение",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Не удалось отправить метрику %1: %2",
ИмяМетрики, ОписаниеОшибки()));
КонецПопытки;
КонецПроцедуры
Функция СформироватьPrometheusМетрику(СтруктураМетрики)
СтрокаМетрики = СтруктураМетрики.metric;
// Добавление меток
Если СтруктураМетрики.Свойство("labels") И СтруктураМетрики.labels.Количество() > 0 Тогда
МассивМеток = Новый Массив;
Для Каждого Метка Из СтруктураМетрики.labels Цикл
МассивМеток.Добавить(СтрШаблон("%1=""%2""", Метка.Ключ, Метка.Значение));
КонецЦикла;
СтрокаМетрики = СтрокаМетрики + "{" + СтрСоединить(МассивМеток, ",") + "}";
КонецЕсли;
// Добавление значения и времени
СтрокаМетрики = СтрокаМетрики + " " + СтруктураМетрики.value;
Если СтруктураМетрики.Свойство("timestamp") Тогда
СтрокаМетрики = СтрокаМетрики + " " + СтруктураМетрики.timestamp;
КонецЕсли;
Возврат СтрокаМетрики + Символы.ПС;
КонецФункции
Health Check и статус API
// HTTP-сервис для проверки состояния
Функция HealthGET(Запрос)
Попытка
НачалоПроверки = ТекущаяУниверсальнаяДата();
СтатусПроверки = Новый Структура;
СтатусПроверки.Вставить("status", "healthy");
СтатусПроверки.Вставить("timestamp", XMLСтрока(НачалоПроверки));
СтатусПроверки.Вставить("version", "1.0.0");
СтатусПроверки.Вставить("checks", Новый Структура);
// Проверка базы данных
ПроверкаБД = ПроверитьСостояниеБазыДанных();
СтатусПроверки.checks.Вставить("database", ПроверкаБД);
// Проверка внешних зависимостей
ПроверкаВнешнихAPI = ПроверитьВнешниеЗависимости();
СтатусПроверки.checks.Вставить("external_apis", ПроверкаВнешнихAPI);
// Проверка дискового пространства
ПроверкаДиска = ПроверитьДисковоеПространство();
СтатусПроверки.checks.Вставить("disk_space", ПроверкаДиска);
// Проверка памяти
ПроверкаПамяти = ПроверитьИспользованиеПамяти();
СтатусПроверки.checks.Вставить("memory", ПроверкаПамяти);
// Общий статус
ОбщийСтатус = "healthy";
Для Каждого Проверка Из СтатусПроверки.checks Цикл
Если Проверка.Значение.status <> "healthy" Тогда
ОбщийСтатус = "unhealthy";
Прервать;
КонецЕсли;
КонецЦикла;
СтатусПроверки.status = ОбщийСтатус;
ВремяПроверки = (ТекущаяУниверсальнаяДата() - НачалоПроверки) * 1000;
СтатусПроверки.Вставить("response_time_ms", ВремяПроверки);
СтрокаJSON = СериализоватьВJSON(СтатусПроверки);
КодОтвета = ?(ОбщийСтатус = "healthy", 200, 503);
Ответ = Новый HTTPСервисОтвет(КодОтвета);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.Заголовки.Вставить("Cache-Control", "no-cache");
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8",
ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
Исключение
СтатусОшибки = Новый Структура;
СтатусОшибки.Вставить("status", "error");
СтатусОшибки.Вставить("error", ОписаниеОшибки());
СтатусОшибки.Вставить("timestamp", XMLСтрока(ТекущаяУниверсальнаяДата()));
СтрокаJSON = СериализоватьВJSON(СтатусОшибки);
Ответ = Новый HTTPСервисОтвет(500);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8",
ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
КонецПопытки;
КонецФункции
Функция ПроверитьСостояниеБазыДанных()
Результат = Новый Структура;
Попытка
НачалоПроверки = ТекущаяУниверсальнаяДата();
// Простой запрос для проверки доступности БД
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 1 КАК Проверка";
Запрос.Выполнить();
ВремяОтклика = (ТекущаяУниверсальнаяДата() - НачалоПроверки) * 1000;
Результат.Вставить("status", "healthy");
Результат.Вставить("response_time_ms", ВремяОтклика);
Результат.Вставить("message", "Database connection OK");
Исключение
Результат.Вставить("status", "unhealthy");
Результат.Вставить("message", "Database connection failed");
Результат.Вставить("error", ОписаниеОшибки());
КонецПопытки;
Возврат Результат;
КонецФункции
Оптимизация производительности
Пулинг соединений и кэширование
// Общий модуль УправлениеHTTPСоединениями
Перем ПулСоединений; // Глобальная переменная модуля
Функция ПолучитьСоединениеИзПула(Сервер, Порт, SSL = Ложь) Экспорт
Если ПулСоединений = Неопределено Тогда
ПулСоединений = Новый Соответствие;
КонецЕсли;
КлючСоединения = СтрШаблон("%1:%2:%3", Сервер, Порт, SSL);
// Проверяем наличие соединения в пуле
Если ПулСоединений.Получить(КлючСоединения) <> Неопределено Тогда
ДанныеСоединения = ПулСоединений.Получить(КлючСоединения);
// Проверяем, не истек ли таймаут соединения
Если ТекущаяУниверсальнаяДата() - ДанныеСоединения.ВремяСоздания < 300 Тогда // 5 минут
ДанныеСоединения.КоличествоИспользований = ДанныеСоединения.КоличествоИспользований + 1;
Возврат ДанныеСоединения.Соединение;
Иначе
// Удаляем устаревшее соединение
ПулСоединений.Удалить(КлючСоединения);
КонецЕсли;
КонецЕсли;
// Создаем новое соединение
Попытка
Если SSL Тогда
SSLСоединение = Новый ЗащищенноеСоединениеOpenSSL();
НовоеСоединение = Новый HTTPСоединение(Сервер, Порт,,,,60, SSLСоединение);
Иначе
НовоеСоединение = Новый HTTPСоединение(Сервер, Порт,,,,60);
КонецЕсли;
// Сохраняем в пул
ДанныеСоединения = Новый Структура;
ДанныеСоединения.Вставить("Соединение", НовоеСоединение);
ДанныеСоединения.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
ДанныеСоединения.Вставить("КоличествоИспользований", 1);
ПулСоединений.Вставить(КлючСоединения, ДанныеСоединения);
Возврат НовоеСоединение;
Исключение
ВызватьИсключение СтрШаблон("Не удалось создать соединение с %1:%2: %3",
Сервер, Порт, ОписаниеОшибки());
КонецПопытки;
КонецФункции
Процедура ОчиститьПулСоединений() Экспорт
Если ПулСоединений = Неопределено Тогда
Возврат;
КонецЕсли;
МассивДляУдаления = Новый Массив;
ТекущееВремя = ТекущаяУниверсальнаяДата();
Для Каждого ЭлементПула Из ПулСоединений Цикл
ДанныеСоединения = ЭлементПула.Значение;
// Удаляем соединения старше 10 минут или неиспользуемые
Если (ТекущееВремя - ДанныеСоединения.ВремяСоздания > 600) ИЛИ
(ДанныеСоединения.КоличествоИспользований = 0) Тогда
МассивДляУдаления.Добавить(ЭлементПула.Ключ);
КонецЕсли;
КонецЦикла;
Для Каждого Ключ Из МассивДляУдаления Цикл
ПулСоединений.Удалить(Ключ);
КонецЦикла;
КонецПроцедуры
Оптимизация JSON сериализации
// Кэширование часто используемых объектов JSON
Перем КэшJSON; // Глобальная переменная модуля
Функция СериализоватьВJSONСКэшем(Данные, КлючКэша = "", ВремяЖизни = 300) Экспорт
Если КэшJSON = Неопределено Тогда
КэшJSON = Новый Соответствие;
КонецЕсли;
// Если указан ключ кэша, проверяем наличие
Если НЕ ПустаяСтрока(КлючКэша) Тогда
КэшированныеДанные = КэшJSON.Получить(КлючКэша);
Если КэшированныеДанные <> Неопределено Тогда
Если ТекущаяУниверсальнаяДата() - КэшированныеДанные.ВремяСоздания < ВремяЖизни Тогда
Возврат КэшированныеДанные.JSON;
Иначе
КэшJSON.Удалить(КлючКэша);
КонецЕсли;
КонецЕсли;
КонецЕсли;
// Сериализация с оптимизированными настройками
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
НастройкиСериализации = Новый НастройкиСериализацииJSON;
НастройкиСериализации.ФорматДаты = ФорматДатыJSON.ISO;
НастройкиСериализации.ВариантЗаписиДаты = ВариантЗаписиДатыJSON.Универсальная;
НастройкиСериализации.ПереносСтрок = ПереносСтрокJSON.Нет; // Минимизируем размер
НастройкиСериализации.ОтступыJSON = ОтступыJSON.Нет; // Убираем отступы
НастройкиСериализации.ЭкранированиеСлешей = ЭкранированиеСлешейJSON.НеЭкранировать;
ЗаписатьJSON(ЗаписьJSON, Данные, НастройкиСериализации);
РезультатJSON = ЗаписьJSON.Закрыть();
// Сохранение в кэш
Если НЕ ПустаяСтрока(КлючКэша) Тогда
КэшированныеДанные = Новый Структура;
КэшированныеДанные.Вставить("JSON", РезультатJSON);
КэшированныеДанные.Вставить("ВремяСоздания", ТекущаяУниверсальнаяДата());
КэшJSON.Вставить(КлючКэша, КэшированныеДанные);
КонецЕсли;
Возврат РезультатJSON;
КонецФункции
Потоковая обработка больших данных
// Функция для обработки больших объемов данных порциями
Функция ОбработатьБольшойЗапросПорциями(Запрос, РазмерПорции = 1000) Экспорт
// Получение общего количества записей
ЗапросПодсчета = Новый Запрос;
ЗапросПодсчета.Текст = СтрЗаменить(Запрос.Текст, "ВЫБРАТЬ", "ВЫБРАТЬ КОЛИЧЕСТВО(*) КАК Количество ИЗ (ВЫБРАТЬ");
ЗапросПодсчета.Текст = ЗапросПодсчета.Текст + ") КАК ВложенныйЗапрос";
// Копируем параметры
Для Каждого Параметр Из Запрос.Параметры Цикл
ЗапросПодсчета.УстановитьПараметр(Параметр.Ключ, Параметр.Значение);
КонецЦикла;
ОбщееКоличество = ЗапросПодсчета.Выполнить().Выгрузить()[0].Количество;
МассивРезультатов = Новый Массив;
Смещение = 0;
Пока Смещение < ОбщееКоличество Цикл
// Модифицируем запрос для получения порции
ТекстЗапросаПорции = Запрос.Текст;
// Добавляем ПЕРВЫЕ и смещение
Если СтрНайти(ВРег(ТекстЗапросаПорции), "ПЕРВЫЕ") = 0 Тогда
ТекстЗапросаПорции = СтрЗаменить(ТекстЗапросаПорции, "ВЫБРАТЬ",
СтрШаблон("ВЫБРАТЬ ПЕРВЫЕ %1 ПРОПУСТИТЬ %2", РазмерПорции, Смещение));
КонецЕсли;
ЗапросПорции = Новый Запрос;
ЗапросПорции.Текст = ТекстЗапросаПорции;
// Копируем параметры
Для Каждого Параметр Из Запрос.Параметры Цикл
ЗапросПорции.УстановитьПараметр(Параметр.Ключ, Параметр.Значение);
КонецЦикла;
РезультатПорции = ЗапросПорции.Выполнить();
Выборка = РезультатПорции.Выбрать();
ПорцияДанных = Новый Массив;
Пока Выборка.Следующий() Цикл
// Преобразование записи в структуру
СтруктураЗаписи = Новый Структура;
Для Колонка = 0 По РезультатПорции.Колонки.Количество() - 1 Цикл
ИмяКолонки = РезультатПорции.Колонки[Колонка].Имя;
ЗначениеКолонки = Выборка[ИмяКолонки];
СтруктураЗаписи.Вставить(ИмяКолонки, ЗначениеКолонки);
КонецЦикла;
ПорцияДанных.Добавить(СтруктураЗаписи);
КонецЦикла;
МассивРезультатов.Добавить(ПорцияДанных);
Смещение = Смещение + РазмерПорции;
// Даем возможность другим процессам выполниться
Если Смещение % (РазмерПорции * 10) = 0 Тогда
Приостановить(50); // 50 мс
КонецЕсли;
КонецЦикла;
Возврат МассивРезультатов;
КонецФункции
Типичные ошибки и их решение
Проблемы с кодировкой
Симптомы: Некорректное отображение русских символов в JSON-ответах, знаки вопросов вместо кириллицы.
Решение:
// НЕПРАВИЛЬНО - не указана кодировка
Ответ.УстановитьТелоИзСтроки(СтрокаJSON);
// ПРАВИЛЬНО - явно указываем UTF-8 без BOM
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8",
ИспользованиеByteOrderMark.НеИспользовать);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ошибки SSL-сертификатов
Симптомы: "Удаленный узел не прошел проверку" при HTTPS-запросах к внешним API.
Решения:
// 1. В файле conf.cfg кластера серверов (только для разработки!)
IgnoreServerCertificatesChainRevocationSoftFail=true
// 2. Программное игнорирование для конкретного соединения (небезопасно!)
SSL = Новый ЗащищенноеСоединениеOpenSSL(,
Новый СертификатыУдостоверяющихЦентровФайл(""));
// 3. ПРАВИЛЬНО - использование собственного хранилища сертификатов
СертификатыУЦ = Новый СертификатыУдостоверяющихЦентровWindows();
SSL = Новый ЗащищенноеСоединениеOpenSSL(, СертификатыУЦ);
// 4. Для проверки конкретного отпечатка сертификата
КлиентскиеСертификаты = Новый МассивКлиентскихСертификатов;
SSL = Новый ЗащищенноеСоединениеOpenSSL(КлиентскиеСертификаты, СертификатыУЦ);
CORS ошибки в веб-приложениях
Симптомы: "Cross-Origin Request Blocked" в консоли браузера, запросы с фронтенда блокируются.
Решение - полная настройка CORS:
// Обработка CORS preflight запросов
Функция КлиентыOPTIONS(Запрос)
Ответ = Новый HTTPСервисОтвет(204); // No Content
// Основные CORS заголовки
Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*"); // или конкретный домен
Ответ.Заголовки.Вставить("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS, PATCH");
Ответ.Заголовки.Вставить("Access-Control-Allow-Headers",
"Content-Type, Authorization, X-API-Key, X-Requested-With");
Ответ.Заголовки.Вставить("Access-Control-Max-Age", "86400"); // 24 часа
// Дополнительные заголовки для credentials
Ответ.Заголовки.Вставить("Access-Control-Allow-Credentials", "true");
Ответ.Заголовки.Вставить("Access-Control-Expose-Headers",
"X-Total-Count, X-Page-Count, Location");
Возврат Ответ;
КонецФункции
// Добавление CORS заголовков во все ответы
Функция ДобавитьCORSЗаголовки(Ответ, Запрос)
Origin = Запрос.Заголовки.Получить("Origin");
// Проверка разрешенных доменов
РазрешенныеДомены = Новый Массив;
РазрешенныеДомены.Добавить("https://myapp.com");
РазрешенныеДомены.Добавить("https://dev.myapp.com");
РазрешенныеДомены.Добавить("http://localhost:3000"); // для разработки
Если НЕ ПустаяСтрока(Origin) И (РазрешенныеДомены.Найти(Origin) <> Неопределено) Тогда
Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", Origin);
Иначе
Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*");
КонецЕсли;
Ответ.Заголовки.Вставить("Access-Control-Allow-Credentials", "true");
Ответ.Заголовки.Вставить("Vary", "Origin");
КонецФункции
Проблемы с таймаутами
Симптомы: Запросы прерываются по таймауту, особенно при работе с медленными внешними API.
Решения:
// 1. Настройка таймаутов на разных уровнях
Соединение = Новый HTTPСоединение("api.external.com", 443,,,,
120, // Таймаут соединения в секундах
SSL);
// 2. Таймаут конкретного запроса (приоритетнее)
Запрос = Новый HTTPЗапрос("/api/slow-endpoint");
Запрос.УстановитьТаймаут(180); // 3 минуты для медленного API
// 3. Адаптивные таймауты в зависимости от операции
Функция ПолучитьТаймаутДляОперации(ТипОперации)
Таймауты = Новый Структура;
Таймауты.Вставить("Авторизация", 30);
Таймауты.Вставить("Получение", 60);
Таймауты.Вставить("Создание", 120);
Таймауты.Вставить("Обновление", 90);
Таймауты.Вставить("Удаление", 60);
Таймауты.Вставить("Отчеты", 300); // 5 минут для отчетов
Возврат Таймауты.Получить(ТипОперации, 60); // По умолчанию 60 сек
КонецФункции
Проблемы с размером данных
Симптомы: Ошибки при передаче больших JSON объектов, превышение лимитов HTTP-сервера.
Решения:
// 1. Сжатие данных
Функция СжатьJSON(СтрокаJSON)
Попытка
ДвоичныеДанные = ПолучитьДвоичныеДанныеИзСтроки(СтрокаJSON, "UTF-8");
СжатыеДанные = СжатьДанные(ДвоичныеДанные);
Возврат СжатыеДанные;
Исключение
ВызватьИсключение "Ошибка сжатия данных: " + ОписаниеОшибки();
КонецПопытки;
КонецФункции
// 2. Пагинация больших результатов
Функция ПолучитьДанныеСПагинацией(ОбщийЗапрос, НомерСтраницы, РазмерСтраницы)
МаксимальныйРазмерСтраницы = 1000; // Ограничение безопасности
РазмерСтраницы = Мин(РазмерСтраницы, МаксимальныйРазмерСтраницы);
Смещение = (НомерСтраницы - 1) * РазмерСтраницы;
// Модификация запроса для пагинации
ТекстЗапроса = СтрЗаменить(ОбщийЗапрос.Текст, "ВЫБРАТЬ",
СтрШаблон("ВЫБРАТЬ ПЕРВЫЕ %1 ПРОПУСТИТЬ %2", РазмерСтраницы, Смещение));
ЗапросСтраницы = Новый Запрос;
ЗапросСтраницы.Текст = ТекстЗапроса;
// Копирование параметров
Для Каждого Параметр Из ОбщийЗапрос.Параметры Цикл
ЗапросСтраницы.УстановитьПараметр(Параметр.Ключ, Параметр.Значение);
КонецЦикла;
Возврат ЗапросСтраницы.Выполнить();
КонецФункции
Версионирование API
Стратегии версионирования
В 1С рекомендуется использовать версионирование через URL path для максимальной совместимости:
// Структура URL с версионированием
// /hs/api/v1/clients - версия 1
// /hs/api/v2/clients - версия 2
// /hs/api/v3/clients - версия 3
Функция ОпределитьВерсиюAPI(Запрос) Экспорт
ОтносительныйURL = Запрос.ОтносительныйURL;
// Извлечение версии из URL
Если СтрНайти(ОтносительныйURL, "/v3/") > 0 Тогда
Возврат "v3";
ИначеЕсли СтрНайти(ОтносительныйURL, "/v2/") > 0 Тогда
Возврат "v2";
ИначеЕсли СтрНайти(ОтносительныйURL, "/v1/") > 0 Тогда
Возврат "v1";
Иначе
// Версия по умолчанию для обратной совместимости
Возврат "v1";
КонецЕсли;
КонецФункции
// Маршрутизация по версиям
Функция КлиентыGET(Запрос)
ВерсияAPI = ОпределитьВерсиюAPI(Запрос);
Если ВерсияAPI = "v1" Тогда
Возврат КлиентыGET_V1(Запрос);
ИначеЕсли ВерсияAPI = "v2" Тогда
Возврат КлиентыGET_V2(Запрос);
ИначеЕсли ВерсияAPI = "v3" Тогда
Возврат КлиентыGET_V3(Запрос);
Иначе
Возврат СформироватьОтветОбОшибке(400, "Неподдерживаемая версия API");
КонецЕсли;
КонецФункции
Управление жизненным циклом версий
// Информация о версиях API
Функция ПолучитьИнформациюОВерсияхAPI() Экспорт
ВерсииAPI = Новый Структура;
// Версия 1.0 - устаревшая (deprecated)
Версия1 = Новый Структура;
Версия1.Вставить("version", "1.0");
Версия1.Вставить("status", "deprecated");
Версия1.Вставить("deprecatedDate", "2024-01-01");
Версия1.Вставить("sunsetDate", "2025-12-31");
Версия1.Вставить("description", "Первая версия API, будет отключена 31.12.2025");
// Версия 2.0 - текущая стабильная
Версия2 = Новый Структура;
Версия2.Вставить("version", "2.0");
Версия2.Вставить("status", "stable");
Версия2.Вставить("releaseDate", "2024-06-01");
Версия2.Вставить("description", "Текущая стабильная версия с расширенными возможностями");
// Версия 3.0 - в разработке (beta)
Версия3 = Новый Структура;
Версия3.Вставить("version", "3.0");
Версия3.Вставить("status", "beta");
Версия3.Вставить("releaseDate", "2025-03-01");
Версия3.Вставить("description", "Новая версия с поддержкой GraphQL и улучшенной безопасностью");
ВерсииAPI.Вставить("v1", Версия1);
ВерсииAPI.Вставить("v2", Версия2);
ВерсииAPI.Вставить("v3", Версия3);
Возврат ВерсииAPI;
КонецФункции
// Предупреждения об устаревших версиях
Функция ДобавитьПредупрежденияОВерсии(Ответ, ВерсияAPI)
ИнформацияОВерсиях = ПолучитьИнформациюОВерсияхAPI();
ТекущаяВерсия = ИнформацияОВерсиях.Получить(ВерсияAPI);
Если ТекущаяВерсия <> Неопределено Тогда
Если ТекущаяВерсия.status = "deprecated" Тогда
Ответ.Заголовки.Вставить("Warning",
СтрШаблон("299 - \"API version %1 is deprecated. Sunset date: %2\"",
ТекущаяВерсия.version, ТекущаяВерсия.sunsetDate));
Ответ.Заголовки.Вставить("Sunset", ТекущаяВерсия.sunsetDate);
ИначеЕсли ТекущаяВерсия.status = "beta" Тогда
Ответ.Заголовки.Вставить("Warning",
СтрШаблон("299 - \"API version %1 is in beta. Subject to change.\"",
ТекущаяВерсия.version));
КонецЕсли;
Ответ.Заголовки.Вставить("API-Version", ТекущаяВерсия.version);
КонецЕсли;
КонецФункции
Миграция между версиями
// Адаптер для совместимости между версиями
Функция АдаптироватьДанныеМеждуВерсиями(Данные, ИсходнаяВерсия, ЦелеваяВерсия) Экспорт
Если ИсходнаяВерсия = ЦелеваяВерсия Тогда
Возврат Данные; // Версии одинаковые
КонецЕсли;
// Миграция с v1 на v2
Если ИсходнаяВерсия = "v1" И ЦелеваяВерсия = "v2" Тогда
Возврат МигрироватьV1наV2(Данные);
КонецЕсли;
// Миграция с v2 на v3
Если ИсходнаяВерсия = "v2" И ЦелеваяВерсия = "v3" Тогда
Возврат МигрироватьV2наV3(Данные);
КонецЕсли;
// Обратная миграция с v2 на v1
Если ИсходнаяВерсия = "v2" И ЦелеваяВерсия = "v1" Тогда
Возврат МигрироватьV2наV1(Данные);
КонецЕсли;
ВызватьИсключение СтрШаблон("Миграция с %1 на %2 не поддерживается",
ИсходнаяВерсия, ЦелеваяВерсия);
КонецФункции
Функция МигрироватьV1наV2(ДанныеV1)
ДанныеV2 = Новый Структура;
// Копируем совместимые поля
Если ДанныеV1.Свойство("id") Тогда
ДанныеV2.Вставить("id", ДанныеV1.id);
КонецЕсли;
Если ДанныеV1.Свойство("name") Тогда
ДанныеV2.Вставить("fullName", ДанныеV1.name); // Переименование поля
КонецЕсли;
// Новые поля в v2 с значениями по умолчанию
ДанныеV2.Вставить("version", "2.0");
ДанныеV2.Вставить("metadata", Новый Структура);
ДанныеV2.metadata.Вставить("migratedFrom", "v1");
ДанныеV2.metadata.Вставить("migrationDate", XMLСтрока(ТекущаяУниверсальнаяДата()));
// Структурирование данных
Если ДанныеV1.Свойство("phone") ИЛИ ДанныеV1.Свойство("email") Тогда
Контакты = Новый Структура;
Если ДанныеV1.Свойство("phone") Тогда
Контакты.Вставить("phone", ДанныеV1.phone);
КонецЕсли;
Если ДанныеV1.Свойство("email") Тогда
Контакты.Вставить("email", ДанныеV1.email);
КонецЕсли;
ДанныеV2.Вставить("contacts", Контакты);
КонецЕсли;
Возврат ДанныеV2;
КонецФункции
Заключение и лучшие практики
REST API в 1С:Предприятие — мощный инструмент для современных интеграций. Платформа предоставляет как автоматические решения через OData, так и гибкие возможности создания собственных HTTP-сервисов.
Ключевые принципы успешной реализации:
🛡️ Безопасность
- Всегда используйте HTTPS в production
- Реализуйте современную аутентификацию (JWT предпочтительнее Basic Auth)
- Применяйте принцип минимальных привилегий
- Логируйте все попытки доступа и подозрительную активность
- Используйте rate limiting для предотвращения злоупотреблений
⚡ Производительность
- Используйте пулы HTTP-соединений для клиентских запросов
- Настройте пулы сессий HTTP-сервисов (
sessionLifetime
,sessionMaxCount
)- Реализуйте кэширование на разных уровнях (данные, JSON, HTTP заголовки)
- Применяйте пагинацию для больших наборов данных
- Оптимизируйте запросы к базе данных
- Используйте сжатие для больших ответов
🔍 Мониторинг и диагностика
- Логируйте все операции с соответствующими уровнями
- Отслеживайте ключевые метрики (время ответа, коды ошибок, throughput)
- Реализуйте health checks для проверки состояния системы
- Используйте correlation ID для трассировки запросов
- Настройте алерты на критические события
🔄 Надежность
- Реализуйте retry механизмы с экспоненциальной задержкой
- Обрабатывайте все типы ошибок (4xx, 5xx, network)
- Используйте circuit breaker для внешних зависимостей
- Настройте адекватные таймауты
- Реализуйте graceful degradation
Чек-лист для production deployment
🚀 Готовность к продакшену:
- ✅ Все endpoints покрыты тестами
- ✅ Настроена аутентификация и авторизация
- ✅ Реализованы rate limiting и throttling
- ✅ Настроено логирование и мониторинг
- ✅ Проведен security audit
- ✅ Настроены health checks
- ✅ Документация API актуальна
- ✅ Настроены алерты и dashboards
- ✅ Проведено нагрузочное тестирование
- ✅ Настроен backup и disaster recovery план
Дальнейшие шаги развития
После освоения базовых принципов REST API в 1С рекомендуется изучить:
- GraphQL интеграции — для более гибкого получения данных
- WebSocket поддержку — для real-time уведомлений
- API Gateway паттерны — для масштабируемых микросервисных архитектур
- Event-driven архитектуру — для асинхронной обработки
- Distributed tracing — для отладки сложных интеграций
- API-first подход — для проектирования новых систем
Полезные ресурсы
- ИТС 1С — официальная техническая поддержка
- Инфостарт — сообщество разработчиков 1С
- MDN HTTP документация — для понимания HTTP протокола
- RESTful API Guide — лучшие практики REST
- JWT.io — всё о JSON Web Tokens
Заключение: REST API в 1С открывает широкие возможности для построения современной IT-архитектуры предприятия. Правильная реализация с учетом принципов безопасности, производительности и надежности позволит создать масштабируемую интеграционную платформу, способную обеспечить бесшовное взаимодействие с внешними системами и поддержать цифровую трансформацию бизнеса.
Дополнительные аспекты
Производственный контур требует внимания к производительности REST API 1С: профилируйте время ответа, оптимизируйте структуру данных, уменьшайте глубину вложенности JSON. Для обработки ошибок REST 1С важно стандартизировать формат ответа и включать correlation-id для трассировки.
При большом количестве вызовов используйте пулы HTTP-сервисов 1С и выставляйте корректные таймауты соединений. Снижайте накладные расходы сериализации за счёт переиспользования буферов. Включайте кэш ETag/If-None-Match и сжатие через reverse-прокси.
Статья опубликована в сентябре 2025 года. Актуально для 1С:Предприятие 8.3.25+.