С появлением технологии ASP.NET Identity от Microsoft .NET разработчики стали все чаще использовать ее при создании веб-приложений. Для краткого экскурса в технологию предлагаем прочитать статью. Эта технология присутствует в стандартном шаблоне проекта и позволяет использовать стандартную реализацию функциональности авторизации и аутентификации пользователя.
«Из коробки» провайдером данных для 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, не нарушая стандартной логики работы с данными таблицами.
Каждый пользователь для UserManager’а предоставляет объект интерфейса IUser. При этом все операции по управлению пользователями производятся через хранилище, представленное объектом IUserStore. Каждая роль представляет реализацию интерфейса IRole, а манипуляции с ролями (добавление, изменение, удаление) осуществляются посредством RoleManager. Непосредственную реализацию интерфейсов IUser, IRole, IUserStore и IRoleStore предоставляет пространство имен Microsoft.AspNet.Identity EntityFramework, где для использования доступны такие классы как IdentityUser, UserStore, IdentityRole, RoleStore, IdentityDbContext.
Если необходимо хранить дополнительную информацию о пользователе, которой нет в указанных таблицах по умолчанию, существует класс 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.
Автор: InterSystems