Урок 2. Аутентификация пользователей в программе
Так как мобильное приложение расширяет возможности десктопного или web-приложения, то оно должно поддерживать многопользовательский режим. При реализации процесса аутентификации пользователей в мобильном приложение важно в первую очередь ответить на вопрос: кто будет пользоваться этим приложением?
Если пользователями приложения будут внутренние сотрудники, то экран входа можно реализовать тем же образом, что делали для десктопного приложения. Пользователь выбирает себя из выпадающего списка и в текстовое поле вводит пароль. Если пользователями будут внешние клиенты, то вход в приложение можно сделать по номеру телефона или электронной почте с отправкой кода для входа.
В качестве учебного проекта будем делать мобильное приложение для внутренних пользователей. Регистрацию внешних пользователей рассмотрим в дополнительных материалах. Т.к. в таком случае нужно регистрироваться на стороннем сервисе, предоставляющем услуги по отправке уведомлений на устройство
При реализации экрана входа нам понадобятся иконки. Скачайте архив с изображениями и разархивируйте его в папку проекта \Template\Projects\1. Template\MobileForms\Images.
Далее вспомним, как хранятся данные пользователей в базе данных и процесс аутентификации пользователей в программе.
Список пользователей
База данных
В базе данных в таблице public.user хранится общий список пользователей системы:

Описание полей таблицы:
user_id - идентификатор глобального пользователя;
user_name - логин глобального пользователя;
user_full_name - полное имя глобального пользователя;
person - признак, определяющий, является ли данный глобальный пользователь реальным пользователем;
enabled - признак, определяющий, является ли данный глобальный пользователь включенным;
language_id - идентификатор языка глобального пользователя;
time_zone_info_id - идентификатор временной зоны глобального пользователя;
user_password - хеш пароля глобального пользователя.
В таблице есть записи системных пользователей:
Служба Workflow Engine ($workflow_engine$) - системный пользователь службы Workflow Engine, от имени которого совершаются некоторые автоматические действия с данными в базе данных;
WS. Гость (WS_GUEST) - системный пользователь, под которым запускаются клиентское приложение для десктопа и мобильное приложение.
Пользователи Администратор, Пользователь 1, Пользователь 2 и Пользователь 3 - реальные пользователи, под которыми можно работать в программе.
Таблица template.user содержит идентификаторы пользователей, которые имеют доступ к бизнес-процессу Template, а так же флаг access_to_mobile_app доступа к мобильному приложению.

Так же в эту таблицу можно добавить поля для индивидуальных пользовательских настроек, например, для доступа к какому-нибудь отчету.
Аутентификация - теория
Первым делом необходимо получить список активных пользователей с доступом к мобильному приложению. Для этого создается отдельный SQL-запрос, который добавляется в Permission доступный для группы GuestGroup.
По умолчанию все запросы к серверу подписываются гостевой учеткой WS_GUEST (находится в группе пользователей GuestGroup), логин и пароль этой учетки зашиты в мобильное приложение WS и не могут быть изменены. В одном из будущих уроков рассмотрим, как персонализировать свое приложение для публикации в магазинах приложений и уделим внимание настройке конфига.
Полученный список пользователей отображается в выпадающем списке на экране входа:


После того, как пользователь выбрал учетную запись и ввел пароль, мобильное приложение хеширует пароль безопасным алгоритмом SHA-512 и отправляет на сервер запрос с данными в зашифрованном виде.
Когда запрос приходит на сервер, он попадает сначала в веб-сервер Kestrel, на котором запущена веб-служба, а затем перенаправляется в серверное приложение.
На сервере механизм аутентификации и авторизации реализован с помощью JWT-токенов. Когда Workflow Engine получает запрос на аутентификацию пользователя, полученные логин и хеш пароля сверяются с теми, которые хранятся в таблице public.user в базе данных. Если логин и хеш пароля совпали - генерируется JWT-токен, который возвращается клиентскому приложению вместе с временем жизни этого токена и одноразовым токеном для повторной генерации основного JWT-токена.
Мобильное приложение хранит JWT-токен и подписывает им все последующие запросы к серверу. По истечении времени жизни JWT-токена клиентская часть отправляет запрос на обновление JWT-токена.
Аутентификация - практика
Список пользователей
Для начала скорректируем таблицу пользователей (template.user), добавив признак доступа к мобильному приложению - не все пользователи WT-программы могут иметь доступ к мобильному приложению:
ALTER TABLE template."user"
ADD COLUMN access_to_mobile_app boolean NOT NULL DEFAULT false;
Выполним запрос, чтобы дать доступ всем активным пользователям:
UPDATE template."user"
SET
access_to_mobile_app = true
FROM
(SELECT
user_id
FROM
template.user_info
WHERE
user_info.person AND NOT user_info.archive) AS user_info
WHERE
"user".user_id = user_info.user_id;
Скорректируем представление template.user_info, добавив в него новое поле:
CREATE OR REPLACE VIEW template.user_info AS
SELECT tu.user_id,
pu.user_name,
pu.user_full_name,
pu.person,
NOT pu.enabled AS archive,
pu.user_id AS public_user_id,
pu.language_id,
l.code AS language_code,
access_to_mobile_app
FROM template."user" tu
JOIN "user" pu ON tu.public_user_id = pu.user_id
JOIN language l ON l.language_id = pu.language_id;
Добавим SQL-запрос в серверный xml-файл
<SqlQuery Name="AppUserLoginSelectSqlQuery">
<Text>
SELECT
user_info.user_id AS "UserId",
user_info.user_name AS "UserName",
user_info.user_full_name AS "UserFullName"
FROM
template.user_info
JOIN template.user_group USING(user_id)
JOIN template."group" USING (group_id)
WHERE
user_info.person AND NOT user_info.archive AND
("group".name != 'GuestGroup' OR "group".name ISNULL) AND
access_to_mobile_app;
</Text>
</SqlQuery>
В существующий BaseViewPermission добавим новый запрос. Это разрешение по умолчанию добавляется во все группы пользователей. Напомним, что в учебном проекте используются динамические права доступа, когда пользователи-администраторы через интерфейс программы назначают права группам пользователей.
Экран входа
Первым делом переименуем существующие файлы: TemplateEmptyStart.xml в TemplateLogin.xml (не забудьте внести правки в таблицу public.mobile_app), а из TemplateEmptySettings.xml удалим Empty. Так же внесем изменения в атрибут Name
тэга <Form>
обоих файлов форм: Для экрана входа укажем TemplateLoginForm, а для экрана настроек - TemplateSettingsForm.
Давайте перейдем в редактор и откроем xml-файл стартового экрана (TemplateLogin.xml).
В файле экрана входа создайте первичное соединение с данными UserPrimaryGetDataConnection для получения списка пользователей из sql-запроса AppUserLoginSelectSqlQuery.
Заменим код объекта LogoPictureBox следующим кодом:
<MyObject Name="LogoPictureBox" Type="PictureBox" Assembly="BaseControls">
<Top>
<DataTypeFormat Type="IntegerDataType" Format="N0">
<Calculate>
<Expression>{0} * 0.2</Expression>
<Items>
<Item>
<Object Name="ContentPanel">
<Property Name="Height" />
</Object>
</Item>
</Items>
</Calculate>
</DataTypeFormat>
</Top>
<Left>30</Left>
<Height>200</Height>
<Width>
<Formula>
<Minus DataType="IntegerDataType">
<Item>
<Object Name="ContentPanel">
<Property Name="Width" />
</Object>
</Item>
<Item>60</Item>
</Minus>
</Formula>
</Width>
<Image>
<Switch>
<Case>
<When>
<Condition Name="AppThemeDarkEqualCondition" />
</When>
<Then>Images\LogoRuDark.png</Then>
</Case>
<Case>Images\LogoRuLight.png</Case>
</Switch>
</Image>
<SizeMode>Zoom</SizeMode>
</MyObject>
Здесь немного изменили размер объекта, а в тэге <Image>
указали выбор файла логотипа по условию AppThemeDarkEqualCondition.


Для выбора пользователя создайте объект с именем UserComboBox типа ComboBox, сделав отступ от LogoPictureBox в 54 единицы и привязав ширину и координату Left к аналогичным значениям LogoPictureBox. В атрибуте Show
тэга <NullValue>
укажите значение False - нам не нужно добавлять в список пустое значение, которое будет отображаться, если значение в списке не выбрано. Вместо этого будем использовать текст-подсказку из тэга <Text>
, в котором укажите текст "Выберите пользователя". В тэге <ValueList>
укажите UserPrimaryGetDataConnection.
В тэге <ForeColor>
выпадающего списка будем использовать цвет в зависимости от выбранного оформления системы. Для темной темы нужно использовать светлый цвет (WhiteSmoke - уже есть на форме), а для светлой темы - темный цвет (MostlyBlack - так же есть на форме). В тэге <BackColor>
так же будем отслеживать оформление системы: для темной темы будем использовать MostlyBlack, а для светлой темы - WhiteSmoke.
Добавьте условие UserComboBoxIsNullCondition, которое будет проверять наличие значения в UserComboBox.
Для ввода пароля добавьте объект с именем PasswordTextBox типа TextBox, задав отступ от UserComboBox в 8 единиц, а ширину и координату Left равными соответствующим свойствам списка. В тэге <TipText>
задается текст-подсказка, который будет отображаться в поле, если значение объекта Null, - укажите "Пароль". А в тэге <TipTextColor>
задается цвет текста-подсказки. Его также будем выбирать в зависимости от оформления системы: для темной темы будем использовать DarkGray, а для светлой темы - LightGray. Для тэгов <ForeColor>
и <BackColor>
задаем значения такие же, как указывали для объекта UserComboBox.
Принято чтобы пароль при вводе скрывался звездочками или точками, а у пользователя была возможность проверить введенные символы, отключая режим пароля у текстового поля.
Для отслеживания активности режима пароля, создадим переменную ShowPasswordVariable:
<MyObject Name="ShowPasswordVariable" Type="Variable" Assembly="SimpleControls">
<Value>True</Value>
</MyObject>
В тэге <Password>
текстового поля PasswordTextBox укажем новый объект.
Для переключения режима пароля создадим объект ShowPasswordPictureBox, который на экране будет представлен в виде классической иконки глаза:
<MyObject Name="ShowPasswordPictureBox" Type="PictureBox" Assembly="BaseControls">
<Top>
<Object Name="PasswordTextBox">
<Property Name="Top" />
</Object>
</Top>
<Right>
<Calculate>
<Expression>{0} - 10</Expression>
<Items>
<Item>
<Object Name="PasswordTextBox">
<Property Name="Right" />
</Object>
</Item>
</Items>
</Calculate>
</Right>
<Height>
<Object Name="PasswordTextBox">
<Property Name="Height" />
</Object>
</Height>
<Width>25</Width>
<Image>
<Switch>
<Case>
<When>
<Object Name="ShowPasswordVariable" />
</When>
<Then>Images\ic_eye.png</Then>
</Case>
<Case>Images\ic_eye_off.png</Case>
</Switch>
</Image>
<SizeMode>Zoom</SizeMode>
</MyObject>
Обратите внимание на то, как задали значение в тэге <Right>
: на экране объект ShowPasswordPictureBox будет отрисовываться поверх объекта PasswordTextBox.
Объекты типа PictureBox не поддерживают вызовы команд, поэтому будем отслеживать событие тапа по объекту, используя условие ClickCondition:
<Condition Name="ShowPasswordPictureBoxClickCondition" Type="ClickCondition" Assembly="Conditions">
<Object Name="ShowPasswordPictureBox" />
</Condition>
Для изменения значения переменной ShowPasswordVariable будем использовать команду типа ValueSetCommand:
<Command Name="ShowPasswordVariableValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
<Object Name="ShowPasswordVariable">
<Not>
<Object Name="ShowPasswordVariable" />
</Not>
</Object>
</Command>
Команду будем вызывать в Execution по условию ShowPasswordPictureBoxClickCondition:
<Execution>
<ConditionExpression>
<Condition Name="ShowPasswordPictureBoxClickCondition" />
</ConditionExpression>
<Commands>
<Command Name="ShowPasswordVariableValueSetCommand" />
</Commands>
</Execution>
Добавьте на экран кнопку LoginButton, сделав отступ от поля ввода пароля в 36 единиц, а левую координату и ширину кнопки привяжите к соответствующим значениям объекта PasswordTextBox. Для тэга <BackColor>
будем использовать один цвет ColorPrimary вне зависимости от выбранной темы оформления. А цвет текста на кнопке (тэг <ForeColor>
) будут зависеть от свойства Enabled самой кнопки. Если кнопка доступна, то цвет текста будет WhiteSmoke, иначе - DarkGray. Для задания радиуса скругления углов кнопки используется тэг <BorderCorner>
со значением 5.
Кнопка "Войти" должна быть активна если выбран пользователь и поле пароля содержит символы:
<Condition Name="MandatoryFieldsNotAllowedNestedCondition" Type="NestedCondition" Assembly="Conditions">
<ConditionExpression>
<Or>
<Condition Name="UserComboBoxIsNullCondition" />
<Condition Name="PasswordTextBoxIsNullOrEmptyCondition" />
</Or>
</ConditionExpression>
</Condition>
Запустите приложение и проверьте реализованную логику:


Отлично! Теперь можем приступить к реализации аутентификации.
Аутентификация
Для аутентификации в программе используется команда типа LoginCommand. Создадим эту команду:
<Command Name="LoginCommand" Type="LoginCommand" Assembly="Commands">
<UserName>
<DataConnection SourceDataConnection="UserSecondaryGetDataConnection">
<Fields>
<Field Name="UserName" />
</Fields>
</DataConnection>
</UserName>
<Password>
<Object Name="PasswordTextBox" />
</Password>
</Command>
В команде используется UserSecondaryGetDataConnection типа SecondaryGetDataConnection, который фильтрует список по идентификатору из UserComboBox и возвращает данные выбранного пользователя.
При вызове команда сама вычислит хеш строки пароля и отправит на сервер логин пользователя и хеш пароля в зашифрованном виде.
Создадим условия проверки результата выполнения команды авторизации:
<Condition Name="LoginCommandOkEqualCondition" Type="EqualCondition" Assembly="Conditions">
<AlwaysChange Value="True" />
<Items>
<Item>
<Command Name="LoginCommand" />
</Item>
<Item>Ok</Item>
</Items>
</Condition>
<Condition Name="LoginCommandFailEqualCondition" Type="EqualCondition" Assembly="Conditions">
<AlwaysChange Value="True" />
<Items>
<Item>
<Command Name="LoginCommand" />
</Item>
<Item>Fail</Item>
</Items>
</Condition>
Создадим команду вывода сообщения об ошибке аутентификации:
<Command Name="LoginFailedMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
<Caption>Ошибка авторизации.</Caption>
<Text>Вы ввели неверный пароль. Пожалуйста, попробуйте еще раз.</Text>
<Buttons Type="Ok" />
</Command>
Создадим необходимые Execution для обработки результатов выполнения команды авторизации:
<Execution>
<ConditionExpression>
<Condition Name="LoginCommandOkEqualCondition" />
</ConditionExpression>
<Commands>
<Command Name="MainFormShowCommand" />
</Commands>
</Execution>
<Execution>
<ConditionExpression>
<Condition Name="LoginCommandFailEqualCondition" />
</ConditionExpression>
<Commands>
<Command Name="LoginFailedMessageBoxCommand" />
</Commands>
</Execution>
Где команда MainFormShowCommand открывает главный экран приложения:
<Command Name="MainFormShowCommand" Type="FormShowCommand" Assembly="Commands">
<Xml Type="Path">TemplateMainForm.xml</Xml>
<Show Type="None" />
</Command>
Создайте пустую форму, которую будем использовать в следующем уроке для построения главного экрана мобильного приложения.
Для создания формы можете использовать паттерн из архива:
В папке \Template\Projects\1. Template\Patterns создайте две вложенные папки Desktop и Mobile. В первую перенесите все существующие паттерны (если такие есть), а во вторую распакуйте архив с шаблонами из урока. Как подключить шаблоны к проекту описано в статье. Если в редакторе остался проект для desktop-приложения, скорректируйте путь до его шаблонов с учетом изменения расположения файлов паттернов.
Использование подключения
Мобильное WT-приложение предполагает подключение к сети во время использования, чтобы иметь возможность обмениваться данными с сервером. Если по каким либо причинам подключение отсутствует, то необходимо об этом уведомить пользователя. Для проверки доступа к Интернету используется универсальное значение <Info>
со значение ConnectivityIsConnected в атрибуте Type
.
<Execution>
<ConditionExpression>
<Not>
<Info Type="ConnectivityIsConnected" />
</Not>
</ConditionExpression>
<Commands>
<Command Name="InternetIssueMessageBoxCommand" />
</Commands>
</Execution>
Где команда InternetIssueMessageBoxCommand имеет вид:
<Command Name="InternetIssueMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
<Caption>Отсутствует интернет-соединение</Caption>
<Text>Попробуйте воспользоваться другой сетью - мобильной или Wi-Fi</Text>
<Buttons Type="Ok" />
</Command>
Обновление данных
Если на форме настроек изменили адрес сервера, то мобильное приложение настроится на новый сервер, но данные на экране входа не обновятся самостоятельно. Для их обновления создадим команду UserPrimaryDataConnectionRefreshCommand типа DataConnectionRefreshCommand для обновления UserPrimaryGetDataConnection. Помимо обновления списка пользователей, необходимо сбрасывать выбранное значение в выпадающем списке UserComboBox и введенные символы в поле PasswordTextBox.
<Command Name="UserPrimaryDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
<DataConnections>
<DataConnection Name="UserPrimaryGetDataConnection" />
</DataConnections>
</Command>
<Command Name="UserComboBoxResetValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
<Object Name="UserComboBox" />
</Command>
<Command Name="PasswordTextBoxResetValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
<Object Name="PasswordTextBox" />
</Command>
Объединим новые команды в одну:
<Command Name="RefreshSequentialCommand" Type="SequentialCommand" Assembly="Commands">
<Commands>
<Command Name="UserComboBoxResetValueSetCommand" />
<Command Name="PasswordTextBoxResetValueSetCommand" />
<Command Name="UserPrimaryDataConnectionRefreshCommand" />
</Commands>
</Command>
В файле формы уже описан Execution, который проверяет результат выполнения команды SettingsFormShowCommand и вызывает команду LogoPictureBoxRestartValueSetCommand на изменения изображения в объекте LogoPictureBox.
Замените имя вызываемой команды на RefreshSequentialCommand:
<Execution>
<ConditionExpression>
<Command Name="SettingsFormShowCommand" Parameter="Updated" />
</ConditionExpression>
<Commands>
<Command Name="RefreshSequentialCommand" />
</Commands>
</Execution>
Удалите из файла описание команды LogoPictureBoxRestartValueSetCommand - она нам больше не пригодится.
Итоги
На уроке мы рассмотрели процесс аутентификации пользователя, .
Ответы
В архиве присутствуют xml-файлы форм для мобильного приложения и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.
Архив не содержит xml-файлы форм десктопного приложения.
Last updated