В нашей конфигурации создадим HTTP-сервис.
Корневой URL у сервиса будет «auth».
.
Создаем шаблон URL с адресом «/service/». Создаем метод POST. Заполняем его свойства и делаем ему обработчик.
В базе данных создаем нового пользователя - AccessTokenGenerationUser. Задаем ему пароль «K3QvQz2y». Пользователь будет иметь свою отдельную роль с правами на вход в 1С и на наш POST-метод в HTTP-сервисе.
Далее создаем еще одного пользователя, под которым мы уже авторизуемся по JWT и получим доступ ко всем нашим сервисам в дальнейшем. Назовем его PublishingDatabaseService. В моем примере данный пользователь имеет полный доступ к взаимодействию со всеми сервисами, включая сервис с именем «Clients». При авторизации по определенному токену в сервис 1С мы авторизуемся как этот пользователь.
Модуль нашего сервиса аутентификации вместе с обработчиком выглядит следующим образом
#Область ОбработкаЗапросов
Функция СервисПолучитьТокен(HTTPЗапрос)
ТелоHTTPОтвета = НовоеТелоHTTPОтвета();
АвторизацияПоТокенамВключена = ПолучитьФункциональнуюОпцию("ЛМС_ИспользоватьТокеныДоступаДляАвторизацииСВнешнимиРесурсами");
Если Не АвторизацияПоТокенамВключена Тогда
ТелоHTTPОтвета.error = НСтр("ru = 'Авторизация по токенам JWT отключена.'", ОбщегоНазначения.КодОсновногоЯзыка());
Возврат HTTPОтвет(ТелоHTTPОтвета, 500);
КонецЕсли;
ТелоЗапроса = HTTPЗапрос.ПолучитьТелоКакСтроку();
ПараметрыЗапроса = ПрочитатьДанныеJSON(ТелоЗапроса);
ИмяПолучателяТокена = Неопределено;
ПараметрыЗапроса.Свойство("AccessTokenRecepientName", ИмяПолучателяТокена);
Если ИмяПолучателяТокена = Неопределено Тогда
ТелоHTTPОтвета.error = НСтр("ru = 'Получатель токена не найден.'", ОбщегоНазначения.КодОсновногоЯзыка());
Возврат HTTPОтвет(ТелоHTTPОтвета, 500);
КонецЕсли;
КлючПодписи = КлючПодписиТокенаДоступа();
ТокенДоступа = ТокенДоступаКВебСервису(ИмяПолучателяТокена, КлючПодписи);
ТелоHTTPОтвета.AccessToken = ТокенДоступа;
Возврат HTTPОтвет(ТелоHTTPОтвета);
КонецФункции
#КонецОбласти // ОбработкаЗапросов
#Область СлужебныеПроцедурыИФункции
Функция НовоеТелоHTTPОтвета()
Возврат Новый Структура("error, accessToken", "", Неопределено);
КонецФункции
Функция ТокенДоступаКВебСервису(ИмяПолучателяТокена, КлючПодписи)
ТокенДоступа = Новый ТокенДоступа;
ТокенДоступа.Заголовки.Вставить("alg", "HS256");
ТокенДоступа.Эмитент = "ssl";
ТокенДоступа.Получатели = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(ИмяПолучателяТокена);
ТокенДоступа.КлючСопоставленияПользователя = "Web_Service";
ТокенДоступа.ВремяСоздания = ТекущаяУниверсальнаяДата() - Дата(1970,1,1,0,0,0);
ТокенДоступа.ВремяЖизни = 60;
ТокенДоступа.Идентификатор = Новый УникальныйИдентификатор;
ТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256, КлючПодписи);
Возврат Строка(ТокенДоступа);
КонецФункции
Функция КлючПодписиТокенаДоступа()
Владелец = ОбщегоНазначения.ИдентификаторОбъектаМетаданных("Константа.ЛМС_ИспользоватьТокеныДоступаДляАвторизацииСВнешнимиРесурсами");
Возврат ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища(Владелец, "КлючПодписи");
КонецФункции
Функция HTTPОтвет(ОтправляемыеДанные, КодСостояния = 200)
Ответ = Новый HTTPСервисОтвет(КодСостояния);
Ответ.Заголовки.Вставить("Accept-Charset", "utf-8");
Ответ.Заголовки.Вставить("Content-Type", "application/json;charset=utf-8");
Ответ.Заголовки["Cache-Control"] = "no-cache";
Ответ.УстановитьТелоИзСтроки(ЗаписатьДанныеJSON(ОтправляемыеДанные));
Возврат Ответ;
КонецФункции
Функция ЗаписатьДанныеJSON(ДанныеДляЗаписи)
ДанныеJSON = "";
Если ДанныеДляЗаписи = Неопределено Тогда
Возврат ДанныеJSON;
КонецЕсли;
Запись = Новый ЗаписьJSON;
Запись.УстановитьСтроку(Новый ПараметрыЗаписиJSON);
ЗаписатьJSON(Запись, ДанныеДляЗаписи);
ДанныеJSON = Запись.Закрыть();
Возврат ДанныеJSON;
КонецФункции
Функция ПрочитатьДанныеJSON(ДанныеJSON, ИменаСвойствСоЗначениямиДата = "")
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(ДанныеJSON);
Возврат ПрочитатьJSON(ЧтениеJSON, , ИменаСвойствСоЗначениямиДата);
КонецФункции
#КонецОбласти // СлужебныеПроцедурыИФункции
Представление кода по созданию токена.
Функция ТокенДоступаКВебСервису(ИмяПолучателяТокена, КлючПодписи)
ТокенДоступа = Новый ТокенДоступа;
ТокенДоступа.Заголовки.Вставить("alg", "HS256");
ТокенДоступа.Эмитент = "ssl";
ТокенДоступа.Получатели = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(ИмяПолучателяТокена);
ТокенДоступа.КлючСопоставленияПользователя = "Web_Service";
ТокенДоступа.ВремяСоздания = ТекущаяУниверсальнаяДата() - Дата(1970,1,1,0,0,0);
ТокенДоступа.ВремяЖизни = 60;
ТокенДоступа.Идентификатор = Новый УникальныйИдентификатор;
ТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256, КлючПодписи);
Возврат Строка(ТокенДоступа);
КонецФункции
Для формирования токена доступа в систему мы воспользуемся новым объектом «ТокенДоступа». Представим описание свойств данного объекта:
Свойства объекта ТокенДоступа |
Свойства HTTP-сервиса в файле default.vrd |
Описание свойства |
Эмитент (соответствует ключу «iss») |
authenticationClaimName |
Указывается как «ssl». Идентификатор приложения, выдавшего токен. |
Получатели (соответствует ключу «aud») |
accessTokenRecepientName |
Указывается идентификатор или любое другое значение в виде строки идентифицируя получателя токена. |
КлючСопоставленияПользователя (соответствует ключу «sub») |
authenticationUserPropertyName |
Указывается имя пользователя, под которым происходит аутентификация. |
ВремяСоздания рассчитывается как ТекущаяУниверсальнаяДата() - Дата(1970,1,1,0,0,0). Это числовое значение времени создания токена доступа в формате UnixTime (количество секунд, прошедших с полуночи 01.01.1970). Соответствует ключам «iat» и «nbf».
ВремяЖизни указывается в секундах. Можно выбрать любую продолжительность жизни токена. Устанавливает значение для ключа «exp» равное сумме времени создания и времени жизни.
КлючПодписи мы используем непосредственно в методе «ТокенДоступа.Подписать», но он также хранится в свойствах файла с именем «keyInformation».
Идентификатор является уникальным идентификатором токена. Соответствует ключу «jti».
Для получения токена мы выполняем обращение к нашему сервису авторизации.
Выполняем запрос с методом POST на адрес «localhost/edo/hs/auth/service/» с идентификационными данными в виде логина и пароля (AccessTokenGenerationUser, K3QvQz2y). Запрос должен включать в себя тело
Свойство AccessTokenRecepientName содержит идентификатор сервиса (к нему будет относиться будущий токен), с которым мы можем обращаться только к сервисам, где в файле default.vrd будет указано это же значение в свойстве AccessTokenRecepientName. В случае успеха получим такой ответ:
Ответ будет состоять из текста ошибки если что-то произошло не так и не удалось сформировать токен и самого токена в виде закодированной строки в формате Base64. Таким образом нам удалось сформировать наш токен и дальше мы можем с ним обратиться к своему HTTP-сервису.
Содержимое полученного токена можно посмотреть с помощью сайта JWT.IO (справочная информация п. 3).
Чтобы сервер аутентификации понял наш запрос с токеном, необходимо скорректировать данные сервиса в файле публикации. Заходим в файл default.vrd и находим там наш сервис (если он уже был опубликован ранее) или добавляем новый.
<service name="Клиенты"
rootUrl="Clients"
enable="true"
reuseSessions="autouse"
sessionMaxAge="20"
poolSize="10"
poolTimeout="5">
<accessTokenAuthentication>
<issuers>
<issuer name="ssl"
authenticationClaimName="sub"
authenticationUserPropertyName="PublishingDatabaseService"
keyInformation="0J/RgNC40LLQtdGCINC80LjRgCE="/>
</issuers>
<accessTokenRecepientName>4e044f77-2563-4f19-bcee-fead012e2584</accessTokenRecepientName>
</accessTokenAuthentication>
</service>
Добавляем в свойства пользователя PublishingDatabaseService информацию о том, что он может быть аутентифицирован с помощью токена доступа.
Отлично, теперь наша публикация откорректирована и мы можем обратиться к нашему сервису clients c целью получения информации о каком-нибудь клиенте.
ПараметрыЗапроса = Новый Структура;
ПараметрыЗапроса.Вставить("clientId", XMLСтрока(КлиентСсылка));
ТелоЗапроса = ЗаписатьДанныеJSON(ПараметрыЗапроса);
ШаблонСтроки = "http://%1/hs/clients/information";
СтрокаURI = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(ШаблонСтроки, "localhost/edo");
СтруктураURI = ОбщегоНазначенияКлиентСервер.СтруктураURI(СтрокаURI);
HTTPЗапрос = Новый HTTPЗапрос(СтруктураURI.ПутьНаСервере);
HTTPЗапрос.УстановитьТелоИзСтроки(ТелоЗапроса);
УстановитьПривилегированныйРежим(Истина);
ИспользоватьТокеныДоступа = ПолучитьФункциональнуюОпцию("ИспользоватьТокеныДоступаДляАвторизацииСВнешнимиРесурсами");
Если ИспользоватьТокеныДоступа Тогда
HTTPЗапрос.Заголовки.Вставить("Authorization", "Bearer " + ТокенДоступаКВебСервису(ИдентификаторВебСервисаИнформацииОКлиентах()));
КонецЕсли;
РезультатВыполнения = ВыполнитьЗапрос(HTTPЗапрос, СтруктураURI, "GET");
Представление кода метода ВыполнитьЗапроса
Функция ВыполнитьЗапрос(HTTPЗапрос, СтруктураАдреса, HTTPМетод = "")
Попытка
HTTPСоединение = НовоеHTTPСоединение(СтруктураАдреса);
Если ПустаяСтрока(HTTPМетод) Тогда
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Иначе
HTTPОтвет = HTTPСоединение.ВызватьHTTPМетод(HTTPМетод, HTTPЗапрос);
КонецЕсли;
Исключение
ЗаписатьОшибкуВЖурналРегистрации(СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(
НСтр("ru = 'Не удалось установить соединение с сервером %1 по причине:
|%2'"), СтруктураАдреса.Хост, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())));
ВызватьИсключение;
КонецПопытки;
Результат = Новый Структура;
Результат.Вставить("ЗапросВыполнен", Ложь);
Результат.Вставить("ОтветСервера", "");
Если HTTPОтвет.КодСостояния = 401
Или HTTPОтвет.КодСостояния = 403 Тогда
ТекстОшибки = НСтр("ru = 'Не удалось пройти авторизацию на сервере.'");
ЗаписатьОшибкуВЖурналРегистрации(ТекстОшибки);
ВызватьИсключение ТекстОшибки;
КонецЕсли;
Результат.ЗапросВыполнен = HTTPОтвет.КодСостояния = 200;
Результат.ОтветСервера = HTTPОтвет.ПолучитьТелоКакСтроку();
Если Не Результат.ЗапросВыполнен Тогда
ЗаписатьОшибкуВЖурналРегистрации(СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(
НСтр("ru = 'Не удалось установить соединение с сервером %1 по причине:
|%2'"), СтруктураАдреса.Хост, Результат.ОтветСервера));
ВызватьИсключение Результат.ОтветСервера;
КонецЕсли;
Возврат Результат;
КонецФункции
Функция НовоеHTTPСоединение(СтруктураURI, Таймаут = 60) Экспорт
ИнтернетПрокси = Неопределено;
Если ОбщегоНазначения.ПодсистемаСуществует("СтандартныеПодсистемы.ПолучениеФайловИзИнтернета") Тогда
МодульПолучениеФайловИзИнтернета = ОбщегоНазначения.ОбщийМодуль("ПолучениеФайловИзИнтернета");
ИнтернетПрокси = МодульПолучениеФайловИзИнтернета.ПолучитьПрокси(СтруктураURI.Схема);
КонецЕсли;
ЗащищенноеСоединение = Неопределено;
Если ВРег(СтруктураURI.Схема) = "HTTPS" Или ВРег(СтруктураURI.Схема) = "FTPS" Тогда
ЗащищенноеСоединение = ОбщегоНазначенияКлиентСервер.НовоеЗащищенноеСоединение();
КонецЕсли;
Соединение = Новый HTTPСоединение(СтруктураURI.Хост, СтруктураURI.Порт, СтруктураURI.Логин, СтруктураURI.Пароль, ИнтернетПрокси, Таймаут, ЗащищенноеСоединение);
Возврат Соединение;
КонецФункции
Справочные материалы
- Что такое токен и как он устроен https://its.1c.ru/db/v8321doc#bookmark:dev:TI000002522;
- Описание полей элемента «accessTokenAuthentication» в файле публикации default.vrd https://its.1c.ru/db/v8321doc#bookmark:adm:TI000001102;
- Для экспериментов с различным наполнением JWT можно использовать сайт https://jwt.io.