Реализация авторизации OAuth 2.0 и JWT в исходящих HTTP-запросах 1С:Предприятие
При интеграции 1С с современными внешними сервисами возникают следующие сложности:
- Внешний API возвращает HTTP-статус
401 Unauthorizedили403 Forbiddenпри отправке стандартных Basic Auth заголовков. - Истечение срока жизни Access Token (
Token expired) во время длительных регламентных заданий. - Необходимость подписи payload для формирования JSON Web Token (JWT) по алгоритму RS256/HS256.
- Ошибки десериализации ответа авторизационного сервера при получении
refresh_token.
1. Архитектура взаимодействия OAuth 2.0 (Client Credentials Flow)
Схема включает два этапа: получение временного access_token через сервисный эндпоинт авторизации и последующее выполнение целевых вызовов с заголовком Authorization: Bearer <токен>. Для минимизации сетевых задержек токен необходимо кэшировать в оперативной памяти или базе данных до момента его экспирации.
2. Программный модуль получения и кэширования Bearer токена
&НаСервере
Функция ПолучитьOAuth2Токен(НастройкиАвторизации, ПринудительноОбновить = Ложь) Экспорт
КлючКэша = "КэшOAuthТокена_" + НастройкиАвторизации.ИмяСервиса;
Если НЕ ПринудительноОбновить Тогда
ДанныеТокена = ПараметрыСеанса.КэшТокеновOAuth.Получить(КлючКэша);
Если ДанныеТокена <> Неопределено И ДанныеТокена.СрокДействия > ТекущаяДатаСеанса() + 60 Тогда
Возврат ДанныеТокена.AccessToken;
КонецЕсли;
КонецЕсли;
// Инициализация SSL соединения
ПараметрыTLS = Новый ПараметрыЗащищенногоСоединенияOpenSSL();
ЗащищенноеСоединение = Новый ЗащищенноеСоединениеOpenSSL(ПараметрыTLS);
HTTPСоединение = Новый HTTPСоединение(
НастройкиАвторизации.СерверАвторизации,
443,
,,,
30,
ЗащищенноеСоединение
);
HTTPЗапрос = Новый HTTPЗапрос(НастройкиАвторизации.ПутьАвторизации);
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/x-www-form-urlencoded");
ТелоЗапроса = СтрШаблон(
"grant_type=client_credentials&client_id=%1&client_secret=%2&scope=%3",
КодироватьСтроку(НастройкиАвторизации.ClientID, СпособКодированияСтроки.КодировкаURL),
КодироватьСтроку(НастройкиАвторизации.ClientSecret, СпособКодированияСтроки.КодировкаURL),
КодироватьСтроку(НастройкиАвторизации.Scope, СпособКодированияСтроки.КодировкаURL)
);
HTTPЗапрос.УстановитьТелоИзСтроки(ТелоЗапроса, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния <> 200 Тогда
ВызватьИсключение СтрШаблон("Ошибка OAuth (%1): %2", HTTPОтвет.КодСостояния, HTTPОтвет.ПолучитьТелоКакСтроку());
КонецЕсли;
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(HTTPОтвет.ПолучитьТелоКакСтроку());
ОтветJSON = ПрочитатьJSON(ЧтениеJSON);
AccessToken = ОтветJSON.access_token;
ExpiresIn = ?(ОтветJSON.Свойство("expires_in"), ОтветJSON.expires_in, 3600);
// Сохранение в кэш
СтруктураКэша = Новый Структура("AccessToken, СрокДействия", AccessToken, ТекущаяДатаСеанса() + ExpiresIn);
ПараметрыСеанса.КэшТокеновOAuth.Вставить(КлючКэша, СтруктураКэша);
Возврат AccessToken;
КонецФункции3. Выполнение целевого запроса с передачей Bearer токена
&НаСервере
Функция ВыполнитьЗапросКРесурсу(Сервер, Путь, ДанныеJSON, НастройкиАвторизации)
Попытка
Токен = ПолучитьOAuth2Токен(НастройкиАвторизации);
Исключение
ЗаписьЖурналаРегистрации("OAuth2", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
ВызватьИсключение;
КонецПопытки;
HTTPСоединение = Новый HTTPСоединение(Сервер, 443,,,, 60, Новый ЗащищенноеСоединениеOpenSSL());
HTTPЗапрос = Новый HTTPЗапрос(Путь);
HTTPЗапрос.Заголовки.Вставить("Authorization", "Bearer " + Токен);
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
HTTPЗапрос.УстановитьТелоИзСтроки(ДанныеJSON, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
HTTPОтвет = HTTPСоединение.ВызватьHTTPМетод("POST", HTTPЗапрос);
// Обработка возможной инвалидации токена до истечения локального TTL
Если HTTPОтвет.КодСостояния = 401 Тогда
Токен = ПолучитьOAuth2Токен(НастройкиАвторизации, Истина); // Принудительный refresh
HTTPЗапрос.Заголовки.Вставить("Authorization", "Bearer " + Токен);
HTTPОтвет = HTTPСоединение.ВызватьHTTPМетод("POST", HTTPЗапрос);
КонецЕсли;
Возврат HTTPОтвет;
КонецФункции4. Декодирование полезной нагрузки JWT (Payload) без внешних библиотек
Для чтения claims из JWT-токена выполните Base64-декодирование центральной секции токена (header.payload.signature):
&НаСервере
Функция ПолучитьPayloadJWT(СтрокаJWT)
МассивЧастей = СтрРазделить(СтрокаJWT, ".");
Если МассивЧастей.Количество() < 2 Тогда
Возврат Неопределено;
КонецЕсли;
Base64Payload = МассивЧастей[1];
// Нормализация Base64URL в стандартный Base64
Base64Payload = СтрЗаменить(Base64Payload, "-", "+");
Base64Payload = СтрЗаменить(Base64Payload, "_", "/");
Остаток = СтрДлина(Base64Payload) % 4;
Если Остаток = 2 Тогда
Base64Payload = Base64Payload + "==";
ИначеЕсли Остаток = 3 Тогда
Base64Payload = Base64Payload + "=";
КонецЕсли;
ДвоичныеДанные = Base64Значение(Base64Payload);
СтрокаJSON = ПолучитьСтрокуИзДвоичныхДанных(ДвоичныеДанные, КодировкаТекста.UTF8);
Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(СтрокаJSON);
Возврат ПрочитатьJSON(Чтение);
КонецФункции Частые вопросы (FAQ)
Как безопасно хранить Client Secret для OAuth в 1С?
Client Secret нельзя жестко зашивать в код конфигурации. Его следует хранить в защищенном регистре сведений с разделением прав или использовать встроенные механизмы БСП 'ХранилищеДополнительнойИнформации' с шифрованием данных.
Что делать, если внешний сервер возвращает JWT в формате Base64Url?
Встроенная функция 1С Base64Значение() работает со стандартным Base64. Символы '-' и '_' необходимо заменить на '+' и '/', а также дополнить строку символами '=' до кратности 4.
Как реализовать подпись JWT алгоритмом RS256 средствами 1С?
Платформа 1С:Предприятие поддерживает объект МенеджерКриптографии. Для RS256 генерируется хеш SHA-256 от Header.Payload и подписывается закрытым RSA-ключом через метод Зашифровать() или внешнюю утилиту OpenSSL.
Почему при получении токена возникает ошибка 'Invalid grant_type'?
Ошибка возникает, если тело POST-запроса не передано в формате Content-Type: application/x-www-form-urlencoded или параметры client_id/client_secret не прошли экранирование через КодироватьСтроку(..., СпособКодированияСтроки.КодировкаURL).