Если у вас есть HTTP-сервис 1С, который отдаёт данные наружу, рано или поздно приходит вопрос: как его закрыть. Basic-авторизация с логином и паролем в каждом запросе — прошлый век: пароль летает по сети постоянно, отозвать доступ одному клиенту нельзя, срок жизни у него бесконечный. Нормальный ответ здесь — JWT-авторизация в 1С: клиент один раз меняет учётку на подписанный токен доступа с ограниченным сроком жизни и дальше носит его в заголовке Authorization. Хорошая новость: с версии платформы 8.3.21 для этого не нужны внешние библиотеки — 1С умеет выпускать и подписывать JWT штатным объектом ТокенДоступа. Разберу на реальном коде, как в 1С сделать и выдачу токена, и — то, что обычно забывают показать, — его проверку на входящем запросе.
Статья собрана из учебных конфигураций: базовой на режиме совместимости 8.3.21 и пары баз «JWT Аутентификация», где два независимых сервиса 1С доверяют друг другу по общему ключу подписи.
Что такое JWT и почему он удобен для 1С
JWT (JSON Web Token) — это строка из трёх частей, разделённых точками: заголовок.нагрузка.подпись. Первые две части — это обычный JSON, закодированный в Base64URL (то есть их может прочитать кто угодно, это не шифрование). Третья часть — подпись: результат HMAC-SHA256 от «заголовок.нагрузка» с секретным ключом, который знает только сервер.
Смысл в том, что сервер не хранит выданные токены. Он их не помнит — он их проверяет. Пришёл запрос с токеном, сервер пересчитал подпись своим ключом, сравнил с той, что в токене: сошлось — токен наш, не сошлось или просрочен — 401. Никакой таблицы сессий, никаких обращений к базе на каждый запрос. Для HTTP-сервиса 1С, который может обслуживать интернет-магазин или мобильное приложение, это заметно проще и дешевле, чем сессионная авторизация.
Что кладут в нагрузку (стандартные поля):
iss(issuer) — кто выпустил токен;sub(subject) — для кого он, обычно имя пользователя;aud(audience) — какие сервисы имеют право его принять;iat/exp— время выпуска и время жизни (в секундах от 1 января 1970);jti— уникальный идентификатор токена.
Выдача токена: нативный объект ТокенДоступа
До 8.3.21 JWT в 1С собирали руками: кодировали заголовок и нагрузку в Base64URL, считали HMAC функцией HMAC, склеивали через точку. Работало, но кода много и легко ошибиться. Начиная с 8.3.21 всё это делает платформа. Вот минимальный обработчик HTTP-сервиса, который выпускает подписанный токен:
Функция TestGET(Запрос)
Ответ = Новый HTTPСервисОтвет(200);
АлгоритмПодписи = АлгоритмПодписиТокенаДоступа.HS256;
ТокенДоступа = Новый ТокенДоступа;
ТокенДоступа.Заголовки.Вставить("alg", Строка(АлгоритмПодписи));
ТокенДоступа.Эмитент = "Test1s";
ТокенДоступа.Получатели.Добавить("ДО8");
ТокенДоступа.КлючСопоставленияПользователя = "admin";
ТокенДоступа.ВремяСоздания = ТекущаяУниверсальнаяДата() - Дата(1970,1,1,0,0,0);
ТокенДоступа.ВремяЖизни = 3600;
ТокенДоступа.Идентификатор = Новый УникальныйИдентификатор;
ТокенДоступа.Подписать(АлгоритмПодписи, Константы.КлючПодписи.Получить());
ТекстТокена = Строка(ТокенДоступа); // готовая JWT-строка header.payload.signature
Ответ.УстановитьТелоИзСтроки(ТекстТокена, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
КонецФункции
Что тут важно понять по шагам:
Новый ТокенДоступасоздаёт пустой токен.АлгоритмПодписиТокенаДоступа.HS256— симметричная подпись одним общим ключом (кроме HS256 платформа поддерживает и асимметричные RS256/ES256, но для «сервер сам себе выдаёт и сам проверяет» HS256 достаточно).ВремяСозданияиВремяЖизнизадаются в секундах от эпохи Unix. РазницуТекущаяУниверсальнаяДата() - Дата(1970,1,1,0,0,0)берём именно от универсальной (UTC) даты — иначе токен «поедет» на величину часового пояса, и клиент увидит, что он то ещё не начал действовать, то уже просрочен.Подписать(...)считает подпись,Строка(ТокенДоступа)отдаёт готовую компактную JWT-строку. Приводить к Base64URL руками не нужно — это делает сама платформа.
В боевом варианте пользователя не хардкодят. Токен выпускают тому, кто аутентифицировался в HTTP-сервисе, — берут текущего пользователя информационной базы:
Функция ПолучитьТокен(Запрос) Экспорт
УстановитьПривилегированныйРежим(Истина);
ПользовательИБ = ПользователиИнформационнойБазы.ТекущийПользователь();
Если ПользовательИБ = Неопределено Тогда
Возврат HTTPОбщегоНазначения.СформироватьОтвет(, "Пользователь не определен", 400);
КонецЕсли;
ДанныеВозврата = Новый Соответствие;
ДанныеВозврата.Вставить("access", СформироватьJWTТокен(ПользовательИБ));
Возврат HTTPОбщегоНазначения.СформироватьОтвет(ДанныеВозврата, , 200, Истина);
КонецФункции
Функция СформироватьJWTТокен(Знач ПользовательИБ)
АлгоритмПодписи = АлгоритмПодписиТокенаДоступа.HS256;
Получатели = Новый Массив;
Получатели.Добавить("jwt1");
Получатели.Добавить("jwt2");
ТокенДоступа = Новый ТокенДоступа;
ТокенДоступа.Заголовки.Вставить("alg", Строка(АлгоритмПодписи));
ТокенДоступа.Эмитент = "jwttest";
ТокенДоступа.КлючСопоставленияПользователя = ПользовательИБ.Имя;
ТокенДоступа.ВремяСоздания = ТекущаяУниверсальнаяДата() - Дата(1970,1,1,0,0,0);
ТокенДоступа.ВремяЖизни = Константы.ВремяЖизниТокена.Получить();
ТокенДоступа.Идентификатор = Новый УникальныйИдентификатор;
ТокенДоступа.ПолезнаяНагрузка.Вставить("token_type", "access");
ТокенДоступа.Получатели = Получатели;
ТокенДоступа.Подписать(АлгоритмПодписи, Константы.КлючПодписи.Получить());
Возврат Строка(ТокенДоступа);
КонецФункции
Здесь КлючСопоставленияПользователя (это поле sub) — имя пользователя ИБ, чтобы при проверке токена понять, от чьего имени пришёл запрос. В ПолезнаяНагрузка можно положить любые свои поля — например, token_type, чтобы отличать access-токен от refresh. Получатели (aud) — список сервисов, которым токен адресован; к нему ещё вернёмся в разделе про доверие двух баз.
Маршруты HTTP-сервиса: /token и /refresh
Сама точка входа — HTTP-сервис с корневым URL /test и шаблонами под выдачу и обновление:
Функция TokenGET(Запрос)
Возврат JWT.ПолучитьТокен(Запрос);
КонецФункции
Функция TokenPOST(Запрос)
Возврат JWT.ПолучитьТокен(Запрос);
КонецФункции
Функция TokenRefreshPOST(Запрос)
Возврат JWT.ПолучитьТокен(Запрос);
КонецФункции
Честно про refresh: в учебной базе /token/refresh просто выпускает новый access-токен тем же кодом — отдельной refresh-логики там нет, это заглушка. В реальном сервисе схема такая: при логине выдают два токена — короткоживущий access (минуты) и долгоживущий refresh (дни). Access носят в каждом запросе, а когда он протух — меняют refresh на новый access, не заставляя пользователя логиниться заново. Отличить их удобно по тому самому token_type в нагрузке, а refresh дополнительно стоит хранить на сервере (в регистре), чтобы иметь возможность его отозвать.
Ответы сервиса стоит завернуть в единый конверт — так клиенту проще разбирать и успех, и ошибку:
Функция СформироватьОтвет(Данные = Неопределено, Ошибка = Неопределено, КодОтвета = 200, ЭтоТокен = Ложь) Экспорт
Ответ = Новый HTTPСервисОтвет(КодОтвета);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Если ЭтоТокен = Ложь Тогда
ДанныеОтвета = Новый Соответствие;
ДанныеОтвета.Вставить("response", Данные);
ДанныеОтвета.Вставить("error", Ошибка);
Иначе
ДанныеОтвета = Данные;
КонецЕсли;
ДанныеJSON = HTTPОбщегоНазначения.ЗаписатьДанныеВJSON(ДанныеОтвета);
Ответ.УстановитьТелоИзСтроки(ДанныеJSON, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
КонецФункции
Проверка входящего токена: чего обычно не показывают
Выдачу токена показывают все. А вот стороны проверки в большинстве примеров нет — и это самое интересное, потому что без неё JWT бесполезен. Платформа отдельного метода «проверь мне этот токен» не даёт, поэтому проверку HS256 собираем вручную — благо это несколько строк. Алгоритм ровно обратный выпуску: разбить токен на три части, пересчитать подпись от «заголовок.нагрузка» своим ключом и сравнить с той, что пришла.
Функция ПроверитьТокен(СтрокаТокена, Ключ) Экспорт
Части = СтрРазделить(СтрокаТокена, ".");
Если Части.Количество() <> 3 Тогда
Возврат Ложь; // это не JWT
КонецЕсли;
ПодписанныйФрагмент = Части[0] + "." + Части[1]; // заголовок.нагрузка
ПодписьИзТокена = Части[2]; // Base64URL-подпись из токена
// пересчитываем HMAC-SHA256 от фрагмента нашим секретным ключом
ДвоичныйКлюч = ПолучитьДвоичныеДанныеИзСтроки(Ключ);
ДвоичныйФрагмент = ПолучитьДвоичныеДанныеИзСтроки(ПодписанныйФрагмент);
ХэшHMAC = HMAC(ДвоичныйКлюч, ДвоичныйФрагмент, ХешФункция.SHA256, 64);
НашаПодпись = ВBase64URL(ХэшHMAC); // приводим к тому же формату, что в токене
Возврат НашаПодпись = ПодписьИзТокена;
КонецФункции
Подпись — это HMAC-SHA256, а не просто SHA256: у неё есть ключ, и штатная функция HMAC(Ключ, Данные, ХешФункция.SHA256, 64) принимает и ключ, и данные как двоичные, четвёртый параметр — размер блока хеш-функции. Результат — двоичные данные, которые нужно закодировать в Base64URL и сравнить со строкой из токена. Base64URL отличается от обычного Base64 тремя мелочами (символы +/ меняются на -_, хвостовые = отбрасываются), это одна вспомогательная функция.
Прочитать нагрузку и проверить срок жизни и получателя после сверки подписи — уже тривиально:
// нагрузка — это Base64URL-JSON во второй части
ДвоичнаяНагрузка = Base64Значение(ДополнитьBase64(Части[1]));
СтрокаНагрузки = ПолучитьСтрокуИзДвоичныхДанных(ДвоичнаяНагрузка, "UTF-8");
Нагрузка = HTTPОбщегоНазначения.ПрочитатьСтрокуJSON(СтрокаНагрузки);
// exp — секунды от эпохи; сравниваем с текущим UTC
СейчасUnix = ТекущаяУниверсальнаяДата() - Дата(1970,1,1,0,0,0);
Если Нагрузка["iat"] + Нагрузка["exp"] < СейчасUnix Тогда
Возврат Ложь; // токен просрочен
КонецЕсли;
И только теперь HTTP-сервис читает Authorization: Bearer <токен> из заголовков запроса, вызывает ПроверитьТокен и либо пускает дальше, либо отвечает 401. Важный момент безопасности: сначала обязательно проверяем подпись и только потом доверяем содержимому нагрузки. Наоборот нельзя — заголовок и нагрузку любой может прочитать и подменить, честность гарантирует только подпись.
Кросс-сервисное доверие: две базы 1С по общему ключу
JWT удобен ещё и тем, что токен, выпущенный одной базой, может принять другая — если у них общий секретный ключ и совпадает список получателей aud. В учебной паре это две почти одинаковые базы, отличающиеся только эмитентом: у первой Эмитент = "jwttest", у второй Эмитент = "ssl". Обе кладут в Получатели один и тот же массив ["jwt1", "jwt2"] и подписывают одним ключом.
Что это даёт на практике. Есть базовый сервис (условно — учётная система) и есть смежный (например, сервис отчётности или обмена). Пользователь логинится в первом, получает токен — и ходит с ним во второй, ничего там повторно не вводя. Второй сервис не спрашивает первый «а этот токен настоящий?» — он просто проверяет подпись своим экземпляром общего ключа. По iss в нагрузке при этом видно, какая база выпустила токен, а по aud — что этот сервис имеет право его принять.
Два условия, без которых доверие не работает:
- ключ подписи должен совпадать в обеих базах — иначе токен, выпущенный первой, не пройдёт проверку во второй. 1С это автоматически не синхронизирует, ключ вы раскладываете сами;
- сервис должен быть в списке
aud— при проверке добавьте сверку, что имя вашего сервиса есть в получателях токена, иначе чужой токен «на предъявителя» пройдёт куда не надо.
Где хранить секрет (и чего не делать)
Главная ошибка учебных примеров — и её видно почти везде — ключ подписи лежит в обычной строковой константе, которую видно через интерфейс. Для демонстрации на вебинаре это нормально, для боевого сервиса — нет. Кто прочитал ключ, тот выпускает любые токены от имени вашего сервиса, и никакая проверка подписи их не отличит от настоящих.
Как правильно:
- храните ключ так, чтобы его нельзя было прочитать из пользовательского режима — в защищённом хранилище (
ХранилищеОбщихНастроекв привилегированном коде), в отдельной константе с чтением только через привилегированный модуль, либо вообще вне базы (переменная окружения, читаемая при публикации); - ключ должен быть длинным и случайным — не «12345» и не название фирмы, а хотя бы 32 случайных байта;
- при компрометации ключ меняют, и все ранее выданные токены разом становятся невалидными — это, кстати, штатный способ «разлогинить всех».
Что в итоге
Собранный минимум JWT-авторизации в 1С — это три вещи: выпуск токена нативным ТокенДоступа с HS256, конечная точка /token (и по-хорошему /refresh), и ручная проверка подписи на входящем запросе через HMAC. Платформа с 8.3.21 закрывает самую скучную часть — кодирование и подпись, — оставляя вам ровно ту логику, которую и надо продумать: срок жизни, хранение ключа и проверку aud. Дальше этот же токен ложится в любой интеграционный сценарий: обмен с сайтом, мобильное приложение, связка микросервисов.
JWT — это способ авторизации поверх интеграции, а не сама интеграция. Если вы делаете полноценный обмен 1С с внешним сервисом на HTTP — как связать 1С и Python-микросервис, где OAuth 2 и токены разбираются на сквозном примере, смотрите на курсе «Python + 1С: интеграция OAuth 2». А как устроен сам HTTP-сервис 1С «под капотом», с приёмом и отдачей JSON, — в разборе микросервиса на FastAPI для 1С.