Многофакторная авторизация. Шаблон проектирования для «1С:Предприятие 8». На примере API «Приватбанка» для юр. лиц

Многим из читателей знакомы понятия стандартов разработки и шаблонов проектирования. Для платформы «1С:Предприятие 8» на сайте its.1c.ru описаны базовые стандарты оформления кода и некоторые полезные примеры, но отсутствует информация об высокоуровневых абстракциях. Почти у каждого банка есть реализация обмена с конфигурациями «1С:Предприятие 8», но анализировать код, а тем более реализацию без слёз невозможно. Данная статья предлагает использовать некий шаблон оформления кода для многофакторной авторизации.

Прежде чем начать

Как тестовый пример будем рассматривать описание API банка «Приватбанк». Детально описание можно найти по ссылке. Данный пример хорош тем, что затрагивает взаимодействие с пользователем, мобильными номерами (OTP-авторизация, правда не во всех случаях), а так же имеется разграничения прав как на уровне приложения так и на уровне доступной роли клиента. Даже беглое описание, наверное, вызывает определенные сложности в оценке времени на разработку, а так же вопросы по поводу организации кода.

Анализируем шаги авторизации

Весь процесс состоит из четырех последовательных шагов:

  1. Получение ID сессии — состоит в том, что бы по R03;ID и R03;secret приложения получить R03;R03;token, который будет передаваться с каждым запросом к Web-сервису банка, в данном случае так же получим права доступа;
  2. Авторизация с помощью пары логин/пароль пользователя — основная цель это повысить права доступа R03;token полученного на предыдущем шаге;
  3. Прохождение OTP-авторизации — если используется двух-факторная авторизация необходимо отправить запрос на получения OTP-пароля, который придет на мобильное устройство;
  4. Проверка OTP-авторизации — отправляем подтверждение повышения прав с помощью OTP-пароля.

Определим объекты которые будут необходимы:

  • Id — идентификатор сессии;
  • ExpiresIn — дата в формате Unix Timestamp, когда истечет сессия;
  • ApplicationId — идентификатор приложения;
  • ApplicationSecret — секрет приложения;
  • Login — логин пользователя;
  • Password — пароль пользователя;
  • OtpDev — устройство для получения OTP-пароля;
  • Otp OTP-пароль полученный на устройство;
  • Роли — таблица ролей доступных сессии.

Срок истечения сессии достаточно мал, получается, что нет смысла выполнять предварительную авторизацию и хранить данные сессии. Логично сохранять только текущую сессию и при каждом запросе к Web-сервису банка проверять валидность сессии и необходимые права доступа к команде сервиса. При надобности обновлять сессию и повышать права к определенному уровню. Теперь можно переходить к псевдокоду.

ШАГ 0. Выбор команды Web-сервиса банка

После предварительного анализа уже понятно, что необходимо для каждого запроса выполнять проверку сессии и если есть потребность проходить авторизацию. Сделаем это с помощью псевдокода для выписок банка:

&НаКлиенте
Процедура ВыпискиПоСчетуНаКлиенте()

ВыпискиИзБанка.Очистить();

ОписаниеОповещения = Новый ОписаниеОповещения(
"ВыполнитьОбновлениеВыписокПоСчету",
ЭтотОбъект,
Новый Структура("Роль", "ROLE_P24_BUSINESS"));

АвторизироватьКлиентБанк(ОписаниеОповещения);

КонецПроцедуры // ВыпискиПоСчетуНаКлиенте()

&НаКлиенте
Процедура ВыполнитьОбновлениеВыписокПоСчету(Результат,
ДополнительныеПараметры = Неопределено) Экспорт

Если Результат = Истина Тогда
ОбновитьДанныеПоСчетуНаСервере();
КонецЕсли;

КонецПроцедуры // ВыполнитьОбновлениеВыписокПоСчету()

Происходит очистка объекта в который будет выполнятся загрузка выписок банка, далее создается описание оповещения, которое будет выполнено при актуальной сессии и доступной роли для данной операции (Роль передается как структура в дополнительных параметрах).

ШАГ 1. Получаем ID сессии

&НаКлиенте
Процедура АвторизироватьКлиентБанк(ОписаниеЭтапаПолученияДанных)

// Очищаем чтобы проверить доступные роли для токена и сравнить с необходимой 
// ролью для выполнения запроса указаного в описании оповещения.
Роли.Очистить();

// Проходим авторизацию приложения
ПриложениеАвторизация = АвторизироватьПриложение();
ОписаниеСледующиегоЭтапаАвторизации = Новый ОписаниеОповещения(
"ВыполнитьПослеАвторизацииПриложения",
ЭтотОбъект,
ПриложениеАвторизация);

ОбработатьРезультатАвторизации(ПриложениеАвторизация,
ОписаниеЭтапаПолученияДанных,
ОписаниеСледующиегоЭтапаАвторизации);

КонецПроцедуры // АвторизироватьКлиентБанк()

&НаСервере
Функция АвторизироватьПриложение()

// Заполняем ApplicationId, ApplicationSecret, Login, Password из БД или данных формы.
ЗаполнитьДанныеАвторизации();

// Id сессии может быть заполнен, тогда при вызове необходимо проверить его валидность
// https://link.privatbank.ua/console/wiki/client_auth Валидация SessionID.
ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
РезультатАвторизации = ОбработкаОбъект.АвторизироватьПриложение(
ApplicationId, ApplicationSecret, Id);
ЗначениеВРеквизитФормы(ОбработкаОбъект, "Объект");

Возврат РезультатАвторизации;

КонецФункции // АвторизироватьПриложение()

После авторизации или валидации приложения, необходимо проверить результат выполнения. Если валидация сессии прошла успешно и роль для вызова операции Web-сервера банка доступна, тогда выполняем запрос на получения полезных данных, иначе переходим на следующий этап авторизации.

&НаКлиенте
Процедура ОбработатьРезультатАвторизации(Результат,
ОписаниеЭтапаПолученияДанных,
ОписаниеСледующиегоЭтапаАвторизаци = Неопределено)

Перем МассивРолей;

Если ТипЗнч(Результат) = Тип("ФиксированнаяСтруктура") Тогда

Результат.Свойство("Id", Id);
Результат.Свойство("ExpiresIn", ExpiresIn);
Если Результат.Свойство("Roles", МассивРолей) Тогда

Роли.Очистить();
Для Каждого ЭлементМассива Из МассивРолей Цикл
Роли.Добавить().Роль = ЭлементМассива;
КонецЦикла;

КонецЕсли;

КонецЕсли;

АвторизацияПройдена = Ложь;
Если ТипЗнч(ОписаниеЭтапаПолученияДанных) = Тип("ОписаниеОповещения") Тогда

ПараметрыОтбора = ОписаниеЭтапаПолученияДанных.ДополнительныеПараметры;
Если ТипЗнч(ПараметрыОтбора) = Тип("Структура") Тогда
Если Роли.НайтиСтроки(ПараметрыОтбора).Количество() > 0 Тогда;
АвторизацияПройдена = Истина;
ВыполнитьОбработкуОповещения(ОписаниеЭтапаПолученияДанных, Истина);
КонецЕсли;
КонецЕсли;

КонецЕсли;

Если АвторизацияПройдена = Ложь Тогда
Если ТипЗнч(ОписаниеСледующиегоЭтапаАвторизаци) = Тип("ОписаниеОповещения") Тогда
ВыполнитьОбработкуОповещения(ОписаниеСледующиегоЭтапаАвторизаци,
ОписаниеЭтапаПолученияДанных);
КонецЕсли;
КонецЕсли;

КонецПроцедуры // ОбработатьРезультатАвторизации()

ШАГ 2. Авторизация с помощью пары логин/пароль

// Процедура выполняется после успешной авторизации приложения и если доступные
// роли не удовлетворяют требованиям выполняемого запроса к клиент-банку.
// 
// Параметры:
//  ОписаниеЭтапаПолученияДанных - ОписаниеОповещения - оповещение которое будет выполнено если 
//                                                      авторизация будет успешной.
//  ПриложениеАвторизация        - Произвольный       - результат предыдущего этапа авторизации.
//              
&НаКлиенте
Процедура ВыполнитьПослеАвторизацииПриложения(ОписаниеЭтапаПолученияДанных,
ПриложениеАвторизация = Неопределено) Экспорт

// Проходим авторизацию клиента
КлиентАвторизация = АвторизироватьКлиента();
ОписаниеСледующиегоЭтапаАвторизации = Новый ОписаниеОповещения(
"ВыполнитьПослеАвторизацииКлиента",
ЭтотОбъект,
КлиентАвторизация);

ОбработатьРезультатАвторизации(КлиентАвторизация,
ОписаниеЭтапаПолученияДанных,
ОписаниеСледующиегоЭтапаАвторизации);

КонецПроцедуры // ВыполнитьПослеАвторизацииПриложения()

&НаСервере
Функция АвторизироватьКлиента()

ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
РезультатАвторизации = ОбработкаОбъект.АвторизироватьКлиента(
Login, Password, Id);
ЗначениеВРеквизитФормы(ОбработкаОбъект, "Объект");

Возврат РезультатАвторизации;

КонецФункции // АвторизироватьКлиента()

После авторизации пользователя, необходимо проверить результат выполнения. Если валидация сессии прошла успешно и роль для вызова операции Web-сервера банка доступна, тогда выполняем запрос на получения полезных данных, иначе переходим на следующий этап авторизации.

ШАГ 3. Прохождение OTP-авторизации

// Процедура выполняется после успешной авторизации клиента и если необходимо
// пройти OTP-авторизацию клиент-банка.
// 
// Параметры:
//  ОписаниеЭтапаПолученияДанных - ОписаниеОповещения - оповещение которое будет выполнено если 
//                                                      авторизация будет успешной.
//  КлиентАвторизация            - Произвольный       - результат предыдущего этапа авторизации.
// 
&НаКлиенте
Процедура ВыполнитьПослеАвторизацииКлиента(ОписаниеЭтапаПолученияДанных,
КлиентАвторизация = Неопределено) Экспорт

Если ТипЗнч(КлиентАвторизация) = Тип("ФиксированнаяСтруктура") Тогда

Если ТипЗнч(КлиентАвторизация.Message) = Тип("ФиксированныйМассив") Тогда

ПараметрыФормы = Новый Структура("НомераТелефонов",
КлиентАвторизация.Message);

ОткрытьФорму("ВнешняяОбработка.Приват24.Форма.ФормаВыбораНомераТелефона",
ПараметрыФормы,
ЭтотОбъект,
,
,
,
Новый ОписаниеОповещения(
"ВыполнитьПослеЗакрытияФормыВыбораТелефона",
ЭтотОбъект,
ОписаниеЭтапаПолученияДанных),
РежимОткрытияОкнаФормы.БлокироватьОкноВладельца);

Иначе

ОткрытьФорму("ВнешняяОбработка.Приват24.Форма.ФормаOTP",
,
ЭтотОбъект,
,
,
,
Новый ОписаниеОповещения(
"ВыполнитьПослеЗакрытияФормыOTP",
ЭтотОбъект,
ОписаниеЭтапаПолученияДанных),
РежимОткрытияОкнаФормы.БлокироватьОкноВладельца);

КонецЕсли;

КонецЕсли;

КонецПроцедуры // ВыполнитьПослеАвторизацииКлиента()

&НаКлиенте
Процедура ВыполнитьПослеЗакрытияФормыВыбораТелефона(РезультатЗакрытия,
ОписаниеЭтапаПолученияДанных = Неопределено) Экспорт

Если ЗначениеЗаполнено(РезультатЗакрытия) Тогда

OtpDev = РезультатЗакрытия;

КлиентАвторизация = АвторизироватьКлиентаОтправитьOTP();
ОписаниеСледующиегоЭтапаАвторизации = Новый ОписаниеОповещения(
"ВыполнитьПослеАвторизацииКлиента",
ЭтотОбъект,
КлиентАвторизация);

ОбработатьРезультатАвторизации(КлиентАвторизация,
ОписаниеЭтапаПолученияДанных,
ОписаниеСледующиегоЭтапаАвторизации);

КонецЕсли;

КонецПроцедуры // ВыполнитьПослеЗакрытияФормыВыбораТелефона() 

ШАГ 4. Проверка OTP-авторизации

На последнем этапе уже нет необходимости создавать описание оповещения для следующего этапе авторизации, этот уже последний.

&НаКлиенте
Процедура ВыполнитьПослеЗакрытияФормыOTP(РезультатЗакрытия,
ОписаниеЭтапаПолученияДанных = Неопределено) Экспорт

Если ЗначениеЗаполнено(РезультатЗакрытия) Тогда

Otp = РезультатЗакрытия;

КлиентАвторизация = АвторизироватьКлиентаПроверитьOTP();
ОбработатьРезультатАвторизации(КлиентАвторизация,
ОписаниеЭтапаПолученияДанных);

КонецЕсли;

КонецПроцедуры // ВыполнитьПослеЗакрытияФормыOTP()

&НаСервере
Функция АвторизироватьКлиентаПроверитьOTP()

ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
РезультатАвторизации = ОбработкаОбъект.АвторизироватьКлиентаПроверитьOTP(
Otp, Id);
ЗначениеВРеквизитФормы(ОбработкаОбъект, "Объект");

Возврат РезультатАвторизации;

КонецФункции // АвторизироватьКлиентаПроверитьOTP()

Если все прошло успешно, будут получены выписки из банка. В случае проблем, ЗначениеВРеквизитФормы использовалось для того что бы формировать вполне симпатичные логи:

10.09.2024 23:36:59:

REQUEST URL
Production Base URL: link.privatbank.ua
Operation: POST /api/auth/createSession

REQUEST BODY
{
"clientId": "*******",
"clientSecret": "*******"
}

10.09.2024 23:36:59:

RESPONSE
{"id":"*******","clientId":"*******","expiresIn":1505079419,"roles":["ROLE_CLIENT"]}

10.09.2024 23:36:59: Выполнен запрос авторизации приложения (279 мс).
10.09.2024 23:36:59: OK: Приложение успешно авторизировано.
10.09.2024 23:36:59:

REQUEST URL
Production Base URL: link.privatbank.ua
Operation: POST /api/p24BusinessAuth/createSession

REQUEST BODY
{
"login": "*******",
"password": "*******",
"sessionId": "*******"
}

10.09.2024 23:37:00:

RESPONSE
{"id":"*******","clientId":"*******","expiresIn":1505079419,"message":"Authentication successfull","roles":["ROLE_P24_BUSINESS","ROLE_CLIENT"]}

10.09.2024 23:37:00: Выполнен запрос авторизации клиента (1 129 мс).
10.09.2024 23:37:00: Authentication successfull
10.09.2024 23:37:00:

REQUEST URL
Production Base URL: link.privatbank.ua
Operation: GET /api/p24b/statements?acc=26006054710862&stdate=10.09.2024&endate=10.09.2024

10.09.2024 23:37:01:

RESPONSE
{}

10.09.2024 23:37:01: Выполнен запрос для получения выписок из банка (552 мс).
10.09.2024 23:37:01: OK: Выписки из банка за указанный период отсутствуют.

Вместо послесловия

Надеюсь, вы заметили шаблон, каждый этап авторизации имеет всего две процедуры или функции: первая процедура предназначена для создания описания оповещения следующего шага авторизации и вызова процедуры/функции/формы текущего шага обработчика, а в самом конце выполняется проверка успешности и переход к получению полезных данных или на следующий шаг авторизации.

Статья в личном блоге клац

6 Comments

  1. koshak84

    Сейчас как раз занимаюсь автоматической загрузкой выписок из 3х разных банков в старую-древнюю УПП. Посмотрел код реализации DirectBank в Библиотеке электронных документов и решил пойти другим путем. Просто в конфигурации БЭД написал обработку, которая с заданной периодичностью запрашивает документы из банков по уже готовым алгоритмам, а потом просто выгружает полученные файлы в УПП. Задача решилась малой кровью. А так да, смотреть без слез на то, что написано в БЭД не возможно.

    Reply
  2. bulpi

    Автор, надо бы выложить обработку. Иначе единственное, что понятно из этой статьи, так это то, что Вы очень умный, а я нет. Если это было целью, то Вы добились своего.

    Reply
  3. Inkasor

    (2)Это шаблон архитектуры, весь код с примерами прямо в статье, какая ещё обработка?

    Reply
  4. bulpi

    В коде есть ссылки на объекты, например, Роли. В коде есть ссылки на формы обработки. Без обработки это не код, а сочинение на вольную тему. Представьте, что Вы читатель статьи, и Вам нужно что-то сделать. Как ? Впрочем, хозяин-барин.

    Reply
  5. pbazeliuk

    (4) Роли это простая структура которая согласуется с описанием АPI (https://link.privatbank.ua/console/wiki/p24business_auth), это не роли конфигурации.

    «Объект» это объект обработки, основной реквизит формы.

    Каждый переход в модуль обработки, это обычный HTTP запрос к сервису банка и преобразование ответа в структурумассив, цель статьи не дать готовое решение, а подумать и развить аналитические навыки.

    Вот, Петр Цап, тоже написал статью над которой необходимо было подумать, и там ни строчки кода, но есть классные идеи.

    Reply
  6. Inkasor

    (5) Спасибо на добром слове 🙂 Я ваши статьи тоже сейчас активно осмысливаю на предмет практического применения, вот эта была прямо вообще в тему.

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *