ASP.NET Identity Caché Provider — працюємо з Identity через InterSystems Caché

З появою технології ASP.NET Identity від Microsoft .NET розробники стали все частіше використовувати її при створенні веб-додатків. Для короткого екскурсу в технологію пропонуємо прочитати статті. Ця технологія присутня у стандартному шаблоні проекту і дозволяє використовувати стандартну реалізацію функціональності авторизації та аутентифікації користувача.

image

«З коробки» провайдером даних для ASP.NET Identity є MSSQL, але оскільки система авторизація Identity може взаємодіяти з будь-якої іншої реляційної СУБД, ми дослідили і реалізували цю можливість для InterSystems Caché.

По-перше, для чого все це? Уявімо, що ваш проект використовує СКБД Caché .NET і вам потрібна повноцінна і надійна система авторизації. Писати таку систему з нуля руками вкрай недоцільно, природно, що ви захочете скористатися існуючим аналогом в .NET — ASP.NET Identity. Але в чистому вигляді фреймворк здатний працювати тільки зі своєю нативної СУБД Microsoft — MS SQL. Наше завдання полягало в тому, щоб реалізувати адаптер, який дозволить легким рухом руки портувати Identity на СУБД Intersystems Cache. Поставлена задача була реалізована в ASP.NET Identity Caché Provider.
Суть проекту ASP.NET Identity Caché Provider полягає в імплементації провайдера даних для Caché ASP.NET Idenity. Основне завдання полягала в зберіганні та наданні доступу до таблиць AspNetRoles, AspNetUserClaims, AspNetUserLogins, AspNetUserRoles AspNetUsers, не порушуючи стандартної логіки роботи з даними таблицями.

Пара слів про архітектуру ASP.NET IdentityКлючовими об'єктами у Asp.Net Identity є користувачі і ролі. Вся функціональність по створенню та видаленню користувачів, взаємодії з сховищем користувачів зберігається в класі UserManager. Для роботи з ролями і їх управлінням Asp.Net Identity визначено клас RoleManager. Нижче представлена діаграма класів Microsoft.AspNet.Identity.Core.

image

Кожен користувач для UserManager'а надає об'єкт інтерфейсу IUser. При цьому всі операції з управління користувачами здійснюються через сховище, представлене об'єктом IUserStore. Кожна роль представляє реалізацію інтерфейсу IRole, а маніпуляції з ролями (додавання, зміна, видалення) здійснюються за допомогою RoleManager. Безпосередню реалізацію інтерфейсів IUser, IRole, IUserStore IRoleStore надає простір імен Microsoft.AspNet.Identity EntityFramework, де для використання доступні такі класи як IdentityUser, UserStore, IdentityRole, RoleStore, IdentityDbContext.
image

Якщо необхідно зберігати додаткову інформацію про користувача, якої немає у зазначених таблицях за замовчуванням, існує клас IdentityUserClaim (клейма), який дозволяє додавати необхідні поля і потім використовувати їх, наприклад, при реєстрації користувача.

Перейдемо до розгляду реалізації провайдера даних для Caché ASP.NET Identity. Вона проходила в два етапи:

− Імплементація класів зберігання даних (які будуть відповідати за зберігання стану) і класу IdentityDbContext, який інкапсулює всю низькорівневу логіку роботи зі сховищем даних. Також був імплементований клас IdentityDbInitializer, який проводить адаптацію бази даних Caché для роботи з Identity.
− Імплементація класів UserStore RoleStore (разом з інтеграційними
тестами). Демонстраційний проект.

В ході першого етапу були имплеменированы наступні класи:
IdentityUser — імплементація інтерфейсу IUser.
IdentityUserRole — асоціативна сутність для зв'язку User–Role.
IdentityUserLogin — дані про логіни користувача.

Розширювана версія класу UserLoginInfo.
IdentityUserClaim — дані про клеймах користувача.
IdentityDbContext<TUser, TRole, TKey, TUserLogin, TUserRole, TUserClaim> — контекст бази даних Entity Framework.

Розглянемо більш докладно сутність IdentityUser, яка являє собою сховище для користувачів, ролей, логінов, клеймов та зв'язків користувач роль. Приклад імплементації звичайного і узагальненого варіанти IdentityUser.

namespace InterSystems.AspNet.Identity.Cache
{
/// <summary>
/// IUser implementation
/ / / < /summary>
public class IdentityUser : IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUser
{
/// <summary>
/// Constructor which creates a new Guid for the Id
/ / / < /summary>
public IdentityUser()
{
Id = Guid.NewGuid().ToString();
}

/// <summary>
/// Constructor that takes a userName
/ / / < /summary>
/ / / < param name="userName"></param>
public IdentityUser(string userName)
: this()
{
UserName = userName;
}
}

/// <summary>
/// IUser implementation
/ / / < /summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TLogin"></typeparam>
/// <typeparam name="TRole"></typeparam>
/// <typeparam name="TClaim"></typeparam>
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
where TLogin : IdentityUserLogin<TKey>
where TRole : IdentityUserRole<TKey>
where TClaim : IdentityUserClaim<TKey>
{
/// <summary>
/// Constructor
/ / / < /summary>
public IdentityUser()
{
Claims = new List<TClaim>();
Roles = new List<TRole>();
Logins = new List<TLogin>();
}

/// <summary>
/// Email
/ / / < /summary>
public virtual string Email { get; set; }

Для реалізації обмеження прав доступу в Identity призначені спеціальні об'єкти – Ролі. Роль в конфігурації може відповідати посадами або видами діяльності різних груп користувачів.

namespace InterSystems.AspNet.Identity.Cache
{
/// <summary>
/// EntityType that represents a user belonging to a role
/ / / < /summary>
public class IdentityUserRole : IdentityUserRole<string>
{
}

/// <summary>
/// EntityType that represents a user belonging to a role
/ / / < /summary>
/// <typeparam name="TKey"></typeparam>
public class IdentityUserRole<TKey>
{
/// <summary>
/// UserId for the user that is in the role
/ / / < /summary>
public virtual TKey UserId { get; set; }

/// <summary>
/// RoleId for the role
/ / / < /summary>
public virtual TKey RoleId { get; set; }
}
}

IdentityDbContext – сутність, инкапсулирующая в собі створення підключення, завантаження сутностей бази даних, валідацію відповідності об'єктів структурі пов'язаних таблиць і полів. В якості прикладу розглянемо метод OnModelCreating, який валидирует таблиці у відповідності з вимогами Identity.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Mapping and configuring identity entities according to the Cache tables
var user = modelBuilder.Entity<TUser>()
.ToTable("AspNetUsers");
user.HasMany(u => u.Roles).WithRequired().HasForeignKey(ur => ur.UserId);
user.HasMany(u => u.Claims).WithRequired().HasForeignKey(uc => uc.UserId);
user.HasMany(u => u.Logins).WithRequired().HasForeignKey(ul => ul.UserId);
user.Property(u => u.UserName)
.IsRequired()
.HasMaxLength(256)
.HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("UserNameIndex") { IsUnique = true }));

user.Property(u => u.Email).HasMaxLength(256);

modelBuilder.Entity<TUserRole>()
.HasKey(r => new { r.UserId, r.RoleId })
.ToTable("AspNetUserRoles");

modelBuilder.Entity<TUserLogin>()
.HasKey(l => new { l.LoginProvider, l.ProviderKey, l.UserId })
.ToTable("AspNetUserLogins");

modelBuilder.Entity<TUserClaim>()
.ToTable("AspNetUserClaims");

var role = modelBuilder.Entity<TRole>()
.ToTable("AspNetRoles");
role.Property(r => r.Name)
.IsRequired()
.HasMaxLength(256)
.HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true }));
role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId);
}

DbModelBuilder служить для зіставлення класів CLR зі схемою бази даних. Цей орієнтований на код підхід до побудови моделі EDM називається Code First. DbModelBuilder зазвичай використовується для налаштування моделі шляхом перевизначення OnModelCreating(DbModelBuilder). Однак DbModelBuilder можна також використовувати незалежно від DbContext для складання моделі і подальшого конструювання DbContext або ObjectContext.

Клас IdentityDbInitializer підготовляє базу даних Caché для використання Identity.

public void InitializeDatabase(DbContext context)
{
using (var connection = BuildConnection(context))
{
var tables = GetExistingTables(connection);

CreateTableIfNotExists(tables, AspNetUsers, connection);
CreateTableIfNotExists(tables, AspNetRoles, connection);
CreateTableIfNotExists(tables, AspNetUserRoles, connection);
CreateTableIfNotExists(tables, AspNetUserClaims, connection);
CreateTableIfNotExists(tables, AspNetUserLogins, connection);
CreateIndexesIfNotExist(connection);
}
}

Методи CreateTableIfNotExists створюють необхідні таблиці, якщо таких ще не існує. Перевірка на існування таблиці робиться за допомогою виконання запиту до таблиці Cache — Dictionary.CompiledClass, в якій зберігається інформація про вже існуючих таблицях. У випадку якщо яка-небудь таблиця ще не створена, вона створюється.

На другому етапі були реалізовані такі сутності як IdentityUserStore і IdentityRoleStore, які інкапсулюють в собі логіку додавання, редагування та видалення користувачів, і ролей. Для цих сутностей потрібно було стовідсоткове покриття юніт-тестів.

Підіб'ємо підсумки: був реалізований провайдер даних для роботи СКБД Caché з Entity Framework в контексті технології ASP.NET Identity. Додаток оформлено в окремий Nuget-пакет, і тепер при необхідності працювати з СКБД Caché, і при цьому використовувати стандартну авторизацію від Microsoft, досить просто впровадити збірку Identity Caché Provider в проект через Nuget Package Manager.

Реалізація проекту з вихідним кодом, прикладом і тестами викладена на GitHub.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.