Перейти к содержанию

Создание типа входа для пользователей

Типы входа (они же - типы аутентификации) указываются для каждого пользователя в карточке сотрудника в соответствующем поле. Они определяют, какие проверки выполняются системой при аутентификации на предмет того, что пользователь корректно указал свои учётные данные (например, логин/пароль или логин/Kerberos).

Доступные по умолчанию типы входа перечислены полями в классе UserLoginTypes. В этом разделе рассмотрено, как добавить новый тип входа и определить для него логику аутентификации.

В примере реализован тип входа “Пользователь CSV”, для которого логин указывается в карточке сотрудника, а пароль определяется по файлу формата csv, в котором перечислены логины и соответствующие им пароли. В примере предполагается, что файл может лежать на сетевом ресурсе и заполняться другой системой.

Important

Для использования в production-окружении также необходимо каким-то образом шифровать пароли и автоматически подхватывать изменения при обновлении файла, а путь к файлу с паролями определять в конфигурационном файле или в карточке настроек. Для упрощения примера такие доработки опущены.

Схема данных

Создайте библиотеку схемы AbSolution, в которой будут содержаться все изменения в схеме, связанные с проектом. Здесь Ab - это короткий префикс проектного решения, который также будет использован в именах типов далее в этом примере. Для вашего решения укажите любой свой префикс.

Все доступные типы входа должны быть указаны в таблице-перечислении LoginTypes. Откроем эту таблицу и добавим в неё строку с значениями в колонках: идентификатор 100, имя Пользователь CSV, библиотека AbSolution.

Important

Для исключения проблем с переходом на новые версии платформы, где могут появиться другие типы входа, для ваших типов всегда используйте числовые идентификаторы 100 и больше.

Tip

Рекомендуется указывать имя для типа входа строкой локализации вида $Enum_LoginTypes_Csv, которую надо добавить в проектную библиотеку локализации. Это позволит как локализовывать на разные языки значение, отображаемое в карточке сотрудника, так и переименовывать его без изменения схемы и необходимости его обновлять для всех сотрудников в колонке PersonalRoles.LoginTypeName по идентификатору в колонке LoginTypeID.

Сохраните схему данных.

Серверные расширения и desktop-клиент

Зарегистрируйте в коде C# тип входа с таким же идентификатором, как и в схеме, и с алиасом Csv. Это не выводимое пользователю имя, а строковый код, используемый при сериализации, в т.ч. в токене сессии. Такая регистрация будет использоваться и на сервере (в веб-сервисе web), и в desktop-клиенте.

В проекте Tessa.Extensions.Shared создайте подпапку Auth, в которую добавьте класс AbLoginTypes с константой:

using Tessa.Platform.Runtime;

namespace Tessa.Extensions.Shared.Auth { public static class AbLoginTypes { public static readonly UserLoginType Csv = new(100, nameof(Csv), UserLoginTypeFlags.UseLogin); } }

Tip

Флаг UserLoginTypeFlags.UseLogin определяет, что идентификация карточки сотрудника будет выполняться по логину, заданному в карточке сотрудника.

Класс регистратора Registrator добавьте в ту же подпапку:

using Tessa.Platform.Runtime;

namespace Tessa.Extensions.Shared.Auth { [Registrator] public sealed class Registrator : RegistratorBase { public override void InitializeRegistration() => UserLoginTypes.All.Add(AbLoginTypes.Csv); } }

В папке проекта Tessa.Extensions.Server добавьте ссылку на NuGet-пакет CsvHelper, который будет использован для чтения файла формата csv. Это можно сделать через интерфейс вашей IDE или добавив в файл проекта Tessa.Extensions.Server.csproj следующий текст:

<ItemGroup> <PackageReference Include="CsvHelper" Version="30.0.1" /> </ItemGroup>

Important

Файл библиотеки CsvHelper.dll требуется копировать в папку веб-сервиса web или в его подпапку extensions. В противном случае сервис аутентификации в этом примере не будет функционировать.

Создайте в проекте Tessa.Extensions.Server подпапку Auth, и добавьте в неё класс CsvAuthenticationService, который будет использован системой для аутентификации пользователей с типом входа с алиасом Csv.

using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; using CsvHelper; using CsvHelper.Configuration; using NLog; using Tessa.Platform; using Tessa.Platform.IO; using Tessa.Platform.Runtime;

namespace Tessa.Extensions.Server.Auth { public sealed class CsvAuthenticationService : AuthenticationServiceBase { private const string UsersFilePath = @"C:\Tessa\users.csv";

private static readonly CsvConfiguration csvConfiguration = new(CultureInfo.InvariantCulture) { Delimiter = "," };

private static readonly Logger logger = LogManager.GetCurrentClassLogger();

private readonly AsyncLazy<Dictionary<string, string>> passwordsByLogins = new(static async () => { var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (!File.Exists(UsersFilePath)) { logger.Error($"Can't read users from \"{UsersFilePath}\": file not found"); return result; }

try { await using var stream = FileHelper.OpenRead(UsersFilePath); using var reader = new StreamReader(stream, leaveOpen: true); using var csv = new CsvReader(reader, csvConfiguration, leaveOpen: true);

while (await csv.ReadAsync()) { if (csv.TryGetField<string>(0, out var loginField) && loginField?.Trim() is { Length: > 0 } login && csv.TryGetField<string>(1, out var passwordField) && passwordField?.Trim() is { Length: > 0 } password) { result[login] = password; } } } catch (Exception ex) { logger.LogException($"Can't read users from \"{UsersFilePath}\"", ex); }

return result; });

protected override async ValueTask<IAuthenticationResponse> LoginCoreAsync( IAuthenticationRequest request, CancellationToken cancellationToken = default) { var password = request.Password; if (string.IsNullOrEmpty(password)) { return AuthenticationResponse.Fail( $"User with login type {request.User.LoginType} doesn't provide a password during login", SessionExceptionCode.UnspecifiedRequiredPasswordAuth); }

var passwordsByLogins = await this.passwordsByLogins.Value; if (!passwordsByLogins.TryGetValue(request.Login, out var expectedPassword) || password != expectedPassword) { return AuthenticationResponse.Fail( RuntimeHelper.InvalidLoginOrPasswordMessage, SessionExceptionCode.InvalidLoginOrPassword); }

return AuthenticationResponse.Success(); } }

protected override ValueTask<bool> CanBlockUserCoreAsync( IAuthenticationRequest request, IAuthenticationResponse response, CancellationToken cancellationToken = default) => new(request.SecurityOptions.BlockWindowsAndLdapUsers); }

В методе CanBlockUserCoreAsync определяется, что после серии неудачных попыток пользователь может быть заблокирован, но только если установлена опция “Выполнять блокировку сотрудников с типом входа Windows или LDAP” в карточке “Настройки сервера” на вкладке “Безопасность”.

  • Метод можно не переопределять, в этом случае для сотрудников с этим типом входа всегда будет возможна блокировка после серии неудачных попыток, если блокировка включена на вкладке “Безопасность” для типа входа “Пользователь TESSA”.
  • Или метод может вернуть new(false), чтобы блокировка никогда не выполнялась (например, если выполняется подключение к внешнему сервису аутентификации, который сам выполняет блокировку пользователей).

Tip

В реальном решении путь к файлу должен не жёстко задаваться в коде, но настраиваться через конфигурационные файлы app.json (получение через объект ConfigurationManager), или же посредством карточки настроек (получение через объект ICardCache). В качестве упрощения здесь путь жёстко задан в константе UsersFilePath.

Добавьте в эту же папку класс Registrator:

using Tessa.Extensions.Shared.Auth; using Tessa.Platform; using Tessa.Platform.Runtime; using Unity;

namespace Tessa.Extensions.Server.Auth { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() => this.UnityContainer.RegisterSingleton<CsvAuthenticationService>();

public override void FinalizeRegistration() => this.UnityContainer.TryResolve<IAuthenticationServiceResolver>() ?.Register<CsvAuthenticationService>(AbLoginTypes.Csv.Name); } }

Расширения web-клиента

В папке проектных расширений web-клиента solution (в сборке она расположена в WebClient SDK/src/solution) создайте подпапку auth. Добавьте в неё файл abLoginTypes.ts с константой Csv, аналогичной реализации в C# AbLoginTypes.cs.

import { UserLoginType, UserLoginTypeFlags } from '@tessa/application';

export namespace AbLoginTypes { export const Csv = new UserLoginType(100, 'Csv', UserLoginTypeFlags.UseLogin); }

Регистрация типа входа в UserLoginTypes должна выполняться перед аутентификацией или другой десериализацией токена (например, если он загружается с сервера по сессии в куках). Поэтому добавим её в регистраторе. Создайте файл registrator.ts в подпапке auth:

import { ExtensionRegistrator, UserLoginTypes } from '@tessa/application'; import { AbLoginTypes } from './abLoginTypes';

export const AbAuthRegistrator: ExtensionRegistrator = { async registerTypes() { UserLoginTypes.add(AbLoginTypes.Csv); }, async registerExtensions(_container) {} };

И пропишите класс регистратора AbAuthRegistrator в файле bundleRegistrator.ts в папке solution:

import { Application } from 'tessa/application'; import { AbAuthRegistrator } from './auth/registrator';

Application.instance.registerBundle({ name: 'Tessa.Extensions.Solution.js', buildTime: process.env.BUILD_TIME!, registry: [ // add registrators here AbAuthRegistrator ] });

Пример CSV-файла с логинами и паролями

Создайте файл формата csv с разделителем “запятая”. Это обычный текстовый файл в кодировке UTF-8, но его также можно создать через Excel, выбрав формат файла через меню “Сохранить как”.

В первой колонке расположим логин пользователя, а во второй - его пароль. Оконечные пробелы при чтении файла игнорируются, а поиск по логину выполняется без учёта регистра (см. логику для значения переменной passwordsByLogins в классе CsvAuthenticationService).

Пример содержимого файла:

user1,test123 user2,test456

Разместите файл по пути, заданному в классе CsvAuthenticationService в константе UsersFilePath. В примере это C:\Tessa\users.csv. Файл должен быть доступен для чтения веб-сервису web.

Tip

Укажите URI-путь к сетевому ресурсу, если доступ требуется обеспечить с нескольких серверов приложений.

Если для разделения колонок в CSV используются не запятые, а другой символ (например, точка с запятой), то укажите его в классе CsvAuthenticationService в свойстве Delimeter.

После каждого редактирования файла веб-сервис web необходимо перезапустить. Прослушивание файловой системы и автоподхват изменений можно реализовать в классе CsvAuthenticationService посредством стандартного класса FileSystemWatcher, но этого не сделано для упрощения примера.

Проверка входа “Пользователь CSV”

Откройте или создайте карточку сотрудника, где в блоке “Общая информация” укажите тип входа “Пользователь CSV”.

В поле “Аккаунт” задайте один из логинов в CSV-файле, например, user1. Сохраните карточку.

Important

Учитывайте, что логин должен быть уникален среди всех пользователей системы без учёта регистра. Поэтому, если такой логин уже указан в другой карточке сотрудника (даже с иным типом входа), то поменяйте его в файле users.csv, чтобы он был уникален.

Теперь выйдите из системы, и в окне логина введите аккаунт (user1) и пароль из csv-файла (в примере - test123).

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

Back to top