Урок 17. Аутентификация пользователей в программе
Last updated
Last updated
Этим уроком мы открываем блок "Многопользовательский режим", в котором рассмотрим механизм аутентификации пользователей, статические и динамические права доступа, пользовательские настройки языка интерфейса и временные зоны.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
До этого урока в приложении мы работали под пользователем WS_GUEST, логин и пароль которого указаны в конфиге клиентской части (WorkflowForms.dll.config). В программе отсутствовала явная аутентификация, и клиентская часть подписывала все запросы к серверу токеном, полученным для гостевой учетной записи. В небольшом проекте без разделения прав доступа такой подход является рабочим.
Но в программе может возникнуть потребность в многопользовательском режиме и необходимость разделения прав доступа для пользователей. Тогда перед разработчиком встает задача реализовать механизм аутентификации в программе, настроить группы пользователей и выдать группам необходимые права доступа.
На этом уроке мы рассмотрим, как происходит аутентификация пользователя в платформе, и реализуем ее в программе, а также рассмотрим настройку прав доступа.
Аутентификация - процедура проверки подлинности, например проверка подлинности пользователя путем сравнения введенного им пароля с паролем, сохраненным в базе данных.
Авторизация - предоставление определенному лицу или группе лиц прав на выполнение определенных действий.
Прежде чем рассказывать про аутентификацию и авторизацию, давайте создадим форму списка пользователей и карточку для редактирования.
В базе данных в таблице 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) - системный пользователь, под которым запускается клиентское приложение.
Пользователь Администратор - реальный пользователь, под которым можно работать в программе.
Таблица template.user содержит идентификаторы пользователей, которые имеют доступ к бизнес-процессу Template.
Так же в эту таблицу можно добавить поля для индивидуальных пользовательских настроек, например, для доступа к какому-нибудь отчету.
Здесь стоит сказать о том, что WT-приложение может объединять несколько программ для разных бизнес-процессов, например, для автомойки и шиномонтажа.
Каждая программа будет иметь свой набор xml-файлов форм и свой серверный xml-файл с запросами. И, в большинстве случаев, серверная часть у них будет одна - нет смысла специально разделать серверные части, если в этом нет потребности. А значит, и базу данных можно использовать одну, но для каждого бизнес-процесса делать отдельную схему, чтобы разделить таблицы и функции.
Несмотря на то, что автомойка и шиномонтаж разные программы, какие-то пользователи могут иметь доступ к обеим программам, например, администраторы. Таким образом, в таблице public.user будут храниться логин и пароль, а так же пользовательские настройки, общие для автомойки и шиномонтажа. А в таблицах carwash.user (автомойка) и tireservice.user (шиномонтажа) будут храниться настройки пользователей, характерные для каждой программы. Например, доступ к какому-нибудь отчету.
Форма списка пользователей должна иметь вид:
Создайте форму для списка пользователей, используя паттерн ArchiveList:
На главной форме (TemplateStart.xml) в меню добавьте пункт Администрирование -> Пользователи..., по которому будет открываться новая форма.
Скорректируем текст запроса на список пользователей:
В запросе используется template.user_info - это представление, которое динамически строится на основе двух таблиц: template.user и public.user.
В PostgreSQL представление (VIEW) - это виртуальная таблица, созданная запросом joins, соединяющим одну или несколько таблиц.
Про представления в PostgreSQL можно почитать в официальном документации по ссылке.
Обратите внимание на условие WHERE: фильтрация идет по полю person - такой проверкой мы отсекаем системных пользователей.
Скорректируем текст запроса для работы с архивными пользователями:
Переменная {UserId}
является системной, и в ней хранится идентификатор пользователя, от которого пришел запрос с клиентской части. Разработчик может переопределить эту переменную, передав с формы одноименный параметр. Но в данном случае для идентификатора редактируемого пользователя будем использовать переменную {EditedUserId}
, чтобы различать идентификаторы и иметь возможность использовать в одном запросе обе переменные.
Отлично, теперь перейдем к карточке пользователя.
Скорректируем запрос на получение данных редактируемого пользователя:
В этом запросе мы как раз используем обе переменные {UserId}
и {EditedUserId}
. И с помощью системной переменной {UserId}
определяем, является ли редактируемый пользователь текущим.
Так как информация о пользователях хранится в двух таблицах template.user и public.user, то и данные о новом пользователе мы должны добавлять в обе:
В таблице template.user_group хранится информация о текущей группе каждого пользователя.
Перейдем в файл карточки пользователя (TemplateUserEdit.xml) и создадим необходимые поля:
Форма будет иметь разный вид при создании и редактировании пользователя. Это необходимо для того, чтобы в целях безопасности не передавать на форму пароль, а точнее его хеш, и не усложнять форму и запрос для обработки пустого поля "Пароль" при редактировании пользователя. Смену пароля существующего пользователя вынесем на отдельную форму. Эту форму будем использовать на форме списка пользователей, чтобы администратор мог сбрасывать пароли, и на главной форме, чтобы пользователь мог сам сменить свой пароль. О разделении прав доступа поговорим позже в этом уроке.
Динамически менять высоту и ширину формы можно через параметры формы Height и Width соответственно. Добавим в тэг <Parameters>
параметр Height с конструкцией <Switch>
для вычисления высоты формы:
Так как имя параметра формы совпадает с именем свойства формы, то форма автоматически подтянет его значение и изменит свой размер. Подобную особенность параметров формы мы уже использовали ранее, когда создавали параметр Title для заголовка формы (HeadLabel) и заголовка окна.
Для объекта GroupComboBox (Группа пользователя) создадим загружающее соединение с данными:
Запрос на получение списка групп пользователей:
В запросе из списка мы исключаем группу "Гости" (GuestGroup) - эта группы будет иметь права доступа только на получение списка пользователей для авторизации в программе.
Всего в программе есть три системных группы пользователей:
Для ввода пароля в текстовое поле PasswordTextBox используйте тэг <Password>
со значением True. В таком случае при вводе текста пользователь будет видеть только символы звездочек ( * ).
В базе данных в таблице public.user в поле user_password хранится не сам пароль, а его хеш, который необходимо вычислять с помощью команды ComputeHashCommand с использованием выбранного алгоритма хеширования. По умолчанию используется алгоритм SHA512.
Создадим такую команду:
Будем вызывать ее в SaveSequentialCommand перед вызовом команды UserInsertSaveCommand, а результат выполнения будем передавать в параметр Password сохраняющего соединения с данными с именем UserInsertSetDataConnection.
При добавлении или редактировании пользователя необходимо делать проверку на уникальность введенного логина.
Создадим запрос для проверки:
Создадим загружающее соединение, чтобы проверять существование пользователя с введенным логином:
И создадим условие для проверки результата выполнения запроса:
Создадим команду на обновление UserExistsPrimaryGetDataConnection:
Также понадобится команда для вывода сообщения пользователю:
Таким образом, команда SaveSequentialCommand будет иметь вид:
Запустите проект и проверьте работу формы, создав несколько пользователей.
При изменении логина текущего пользователя (поле IsCurrent в запросе UserByIdSelectSqlQuery) необходимо чтобы пользователь самостоятельно повторно зашел в программу с новым логином.
Чтобы предупреждать пользователя об изменении логина текущей учетки, добавьте кнопку "Редактировать логин" в карточку пользователя:
При нажатии на кнопку текстовое поле "Логин" должно становиться доступный для изменения, и пользователю должно отображаться предупреждение вида:
В качестве самостоятельной работы можете решить "задачку со звездочкой".
Пока мы просто предупреждаем пользователя, о необходимости повторно войти в программу при изменении логина у текущего пользователя программы. Но при возвращении на форму списка пользователей упадет ошибка вида:
Запрос "UserSelectSqlQuery" в процессе "Template" не может быть выполнен, т.к. пользователь "administrator" не найден или отключен.
Реализуйте самостоятельно логику, чтобы в подобном случае пользователь сразу возвращался на стартовую форму, где открывалась бы форма входа в программу.
Используйте материал из раздела Аутентификация - практика, там будем создавать команду ReloginFormShowCommand для повторного входа в программу.
Первым делом необходимо получить список активных пользователей. Для этого создается отдельный SQL-запрос, который доступен для группы GuestGroup.
По умолчанию все запросы к серверу подписываются гостевой учеткой WS_GUEST (находится в группе пользователей GuestGroup), логин и пароль которой указаны в файле WorkflowForms.dll.config.
Полученный список пользователей отображается в выпадающем списке на форме входа:
После того, как пользователь выбрал учетную запись и ввел пароль, клиентская часть хеширует пароль безопасным алгоритмом SHA-512 и отправляет на сервер запрос с данными в зашифрованном виде.
Когда запрос приходит на сервер, он попадает сначала в веб-сервер Kestrel, на котором запущена веб-служба, а затем перенаправляется в серверное приложение.
Kestrel представляет кроссплатформенный веб-сервер и по умолчанию включается в проект ASP.NET Core.
На сервере механизм аутентификации и авторизации реализован с помощью JWT-токенов. Когда Workflow Engine получает запрос на аутентификацию пользователя, полученные логин и хеш пароля сверяются с теми, которые хранятся в таблице public.user в базе данных. Если логин и хеш пароля совпали - генерируется JWT-токен, который возвращается клиентскому приложению вместе с временем жизни этого токена и одноразовым токеном для повторной генерации основного JWT-токена.
JWT (или JSON Web Token) представляет собой веб-стандарт, который определяет способ передачи данных о пользователе в формате JSON в зашифрованном виде.
Клиентское приложение хранит JWT-токен и подписывает им все последующие запросы к серверу. По истечении времени жизни JWT-токена клиентская часть отправляет запрос на обновление JWT-токена.
Создадим пустую форму для входа в программу (TemplateLogin.xml):
Перейдем в файл главной формы (TemplateStart.xml) и создадим две команды на открытие формы входа:
Команду LoginFormShowCommand будем вызывать при старте приложения для первичного входа в приложение.
Для вызова команды ReloginFormShowCommand на стартовой форме добавим в главное меню пункт Файл -> Войти как..., чтобы у пользователей была возможность входа в программу под другой учеткой без необходимости закрывать приложение.
Также это позволит нам просматривать форму входа при ее редактировании.
Нам понадобятся иконки. Скачайте архив с изображениями и разархивируйте его в папку проекта \Template\Projects\1. Template\Forms\Images\24x24.
Создайте на форме выпадающий список пользователей (UserComboBox) и текстовое поле для ввода пароля (PasswordTextBox).
Для получения списка активных пользователей создадим запрос:
Создадим разрешение, которое будет предоставлять доступ к запросам, общим для всех групп пользователей:
Добавим роль для базовых прав доступа:
Добавим эту роль в группу GuestGroup.
Вернемся на форму входа (TemplateLogin.xml) и создадим соединения с данными для получения списка активных пользователей и для фильтрации выбранного пользователя:
Для аутентификации в программе используется команда типа LoginCommand. Создадим эту команду:
Команда сама вычислит хеш строки пароля и отправит на сервер логин пользователя и хеш пароля в зашифрованном виде.
Создадим условия проверки результата выполнения команды авторизации:
Создадим команду вывода сообщения об ошибке аутентификации:
Вызывать команду LoginCommand будем по кнопке LoginButton. Также можно выполнять вызов по нажатию клавиши Enter при вводе пароля в текстовом поле PasswordTextBox. Для проверки нажатия клавиши будем использовать условие KeyPressCondition:
Создадим необходимые Execution для обработки нажатия клавиши Enter и для обработки результатов выполнения команды авторизации:
Отлично!
Когда пользователь запускает приложение, первое что он увидит, должна быть форма входа в программу. Но делать форму TemplateLogin.xml стартовой - не самая лучшая идея.
Связано это с тем, что стартовая форма является главным окном программы. Когда закрывается главное окно, то закрываются все дочерние окна программы. Следовательно, мы не сможем закрывать форму авторизации после входа в программу.
Сворачивать стартовую форму авторизации тоже будет не самым лучшим решением, так как при закрытии главной формы по крестику пользователь надеется закрыть программу. Но вместо этого он попадет на форму авторизации.
Нам остается только делать хитрый финт: запускать главную форму (TemplateStart.xml) без обновления данных, делать ее невидимой и открывать форму входа. Далее на главной форме проверять значение параметра Updated дочерней формы. Если значение будет False, то закрывать главную форму и прекращать работу программы, а если будет True, то делать главную форму видимой и обновлять данные.
Для начала нужно перевести все PrimaryGetDataConnection в ручной режим загрузки (ManualLoad=True) и добавить команду AllPrimaryGetDataConnectionRefreshCommand для их обновления:
Не добавляйте в команду OrderPrimaryGetDataConnection. Для его обновления уже есть команда OrderDataConnectionRefreshCommand - будем использовать ее, так как в будущих уроках пригодится разделение на разные команды.
Чтобы форма была невидимой (прозрачной), в тэг <Form>
добавим атрибут Opacity
со значением 0. А для изменения прозрачности формы создадим команду:
Добавим Execution для открытия формы входа и обработки результата:
У нас еще есть команда ReloginFormShowCommand для аутентификации в момент работы в программе. Давайте добавим Execution для обработки результата выполнения этой команды:
Отлично! Запустите приложение и проверьте отображение формы входа. Попробуйте войти в программе.
После успешной аутентификации при попытке обновить данные сервер выбросит ошибку вида:
Пока что мы работали в программе под пользователем WS_GUEST и все права доступа на запросы мы добавляли в GuestGroup. Но это гостевая учетка, и ее права доступа должны быть ограничены, если в программе поддерживается многопользовательский режим и разделение прав доступа.
Для реальных пользователей есть группы AdministratorGroup и UserGroup. Давайте добавим эти группы в серверный xml-файл. Затем скопируем в них роли из GuestGroup, в которой оставим только BaseRole, чтобы при запуске приложения на форме входа мы могли видеть список активных пользователей.
Подробнее о правах доступа поговорим в следующем уроке, где рассмотрим рекомендации по формированию Permission и разделению их на роли.
Запустите программу и проверьте аутентификации и загрузку данных.
В программе должна быть возможность менять пароль пользователей через интерфейс.
На форме списка пользователей (TemplateUserList.xml) создайте кнопку для вызова формы смены пароля. Для кнопки используйте изображение password.png из ранее скачанного архива.
Самостоятельно создайте форму смены пароля (TemplateUserPasswordEdit.xml) вида:
На форму смены пароля необходимо передавать идентификатор пользователя, которому меняется пароль. Создайте для этого параметр EditedUserId. Используйте его в запросе UserByIdSelectSqlQuery для получения логина редактируемого пользователя и флага IsCurrent.
По кнопке сохранить необходимо вычислять хеш нового пароля и сохранять изменение в базу данных. Если меняется пароль для текущего пользователя (поле IsCurrent в запросе UserByIdSelectSqlQuery), то необходимо вызывать команду LoginCommand для повторной аутентификации с новым паролем.
Создадим запрос для смены пароля:
Создайте новое разрешение UserPasswordEditSqlQueryPermission, в которое добавьте запросы UserByIdSelectSqlQuery и UserPasswordUpdateSqlQuery. Разрешение добавьте в роль UserPasswordRole, которую укажите для каждой группы.
Текущий пользователь должен иметь возможность поменять свой пароль без необходимости заходить в список пользователей. Особенно, если у пользователя нет прав доступа к списку. Разделение прав доступа на форме рассмотрим в следующих уроках.
На главной форме (TemplateStart.xml) в меню добавьте пункт Файл -> Сменить пароль... для открытия формы изменения пароля для текущего пользователя.
Чтобы на форму смены пароля передавать идентификатор текущего пользователя (параметр EditedUserId) создайте на главной форме соединение с данными:
Обратите внимание, что мы указали тэг <ManualLoad>
со значение True, так как нам не нужно обновлять данные о текущем пользователе до тех пор, пока пользователь не прошел аутентификацию в программе.
Добавим соединение с данными в команду AllPrimaryGetDataConnectionRefreshCommand, чтобы оно обновлялось при успешной аутентификации пользователя.
В серверный xml-файл скопируем текст запроса UserCurrentSelectSqlQuery:
Добавим запрос в BaseViewSqlQueryPermission, так как он является общим для всех пользователей.
Запустите приложение и проверьте загрузку формы и смену пароля.
На уроке мы рассмотрели процесс аутентификации пользователя, добавили возможность редактировать список пользователей, а также настроили статические права доступа для групп "Администраторы" и "Пользователи".
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.