Создание типа входа для пользователей¶
Типы входа (они же - типы аутентификации) указываются для каждого пользователя в карточке сотрудника в соответствующем поле. Они определяют, какие проверки выполняются системой при аутентификации на предмет того, что пользователь корректно указал свои учётные данные (например, логин/пароль или логин/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
).
Система успешно выполнит вход для этого сотрудника. Если же указать другой пароль или отсутствующий в файле логин, то пользователю будет сообщено, что логин или пароль некорректны.