Многим из читателей знакомы понятия стандартов разработки и шаблонов проектирования. Для платформы «1С:Предприятие 8» на сайте its.1c.ru описаны базовые стандарты оформления кода и некоторые полезные примеры, но отсутствует информация об высокоуровневых абстракциях. Почти у каждого банка есть реализация обмена с конфигурациями «1С:Предприятие 8», но анализировать код, а тем более реализацию без слёз невозможно. Данная статья предлагает использовать некий шаблон оформления кода для многофакторной авторизации.
Прежде чем начать
Как тестовый пример будем рассматривать описание API банка «Приватбанк». Детально описание можно найти по ссылке. Данный пример хорош тем, что затрагивает взаимодействие с пользователем, мобильными номерами (OTP-авторизация, правда не во всех случаях), а так же имеется разграничения прав как на уровне приложения так и на уровне доступной роли клиента. Даже беглое описание, наверное, вызывает определенные сложности в оценке времени на разработку, а так же вопросы по поводу организации кода.
Анализируем шаги авторизации
Весь процесс состоит из четырех последовательных шагов:
- Получение ID сессии — состоит в том, что бы по
ID
иsecret
приложения получить token
, который будет передаваться с каждым запросом к Web-сервису банка, в данном случае так же получим права доступа; - Авторизация с помощью пары логин/пароль пользователя — основная цель это повысить права доступа
token
полученного на предыдущем шаге; - Прохождение OTP-авторизации — если используется двух-факторная авторизация необходимо отправить запрос на получения OTP-пароля, который придет на мобильное устройство;
- Проверка 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()
Если все прошло успешно, будут получены выписки из банка. В случае проблем, ЗначениеВРеквизитФормы использовалось для того что бы формировать вполне симпатичные логи:
[code lang=text]
10.09.2017 23:36:59:
REQUEST URL
Production Base URL: link.privatbank.ua
Operation: POST /api/auth/createSession
REQUEST BODY
{
"clientId": "*******",
"clientSecret": "*******"
}
10.09.2017 23:36:59:
RESPONSE
{"id":"*******","clientId":"*******","expiresIn":1505079419,"roles":["ROLE_CLIENT"]}
10.09.2017 23:36:59: Выполнен запрос авторизации приложения (279 мс).
10.09.2017 23:36:59: OK: Приложение успешно авторизировано.
10.09.2017 23:36:59:
REQUEST URL
Production Base URL: link.privatbank.ua
Operation: POST /api/p24BusinessAuth/createSession
REQUEST BODY
{
"login": "*******",
"password": "*******",
"sessionId": "*******"
}
10.09.2017 23:37:00:
RESPONSE
{"id":"*******","clientId":"*******","expiresIn":1505079419,"message":"Authentication successfull","roles":["ROLE_P24_BUSINESS","ROLE_CLIENT"]}
10.09.2017 23:37:00: Выполнен запрос авторизации клиента (1 129 мс).
10.09.2017 23:37:00: Authentication successfull
10.09.2017 23:37:00:
REQUEST URL
Production Base URL: link.privatbank.ua
Operation: GET /api/p24b/statements?acc=26006054710862&stdate=10.09.2017&endate=10.09.2017
10.09.2017 23:37:01:
RESPONSE
{}
10.09.2017 23:37:01: Выполнен запрос для получения выписок из банка (552 мс).
10.09.2017 23:37:01: OK: Выписки из банка за указанный период отсутствуют.
[/code]
Вместо послесловия
Надеюсь вы заметили шаблон, каждый этап авторизации имеет всего две процедуры или функции: первая процедура предназначена для создания описания оповещения следующего шага авторизации и вызова процедуры/функции/формы текущего шага обработчика, а в самом конце выполняется проверка успешности и переход к получению полезных данных или на следующий шаг авторизации.
Спасибо. Пока не пробовал, но кажется очень полезным.
Да не за что, специально не выкладываю весь код, чтобы всегда можно было немного подумать.
кгам.
безполезнаяя статья.
если есть время украшать код, значит ты лентяй
Наверное, вы делаете что-то недорого и быстро, а я делаю качественно и очень дорого.