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 — контекст базы данных Entity Framework.

Рассмотрим более подробно сущность IdentityUser, которая представляет собой хранилище для пользователей, ролей, логинов, клеймов и связей пользователь-роль. Пример имплементации обычного и обобщенного варианта IdentityUser.

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

        /// 
        /// Constructor that takes a userName
        /// 
        /// 
        public IdentityUser(string userName)
            : this()
        {
            UserName = userName;
        }
    }

    /// 
    /// IUser implementation
    /// 
    /// 
    /// 
    /// 
    /// 
    public class IdentityUser : IUser
        where TLogin : IdentityUserLogin
        where TRole : IdentityUserRole
        where TClaim : IdentityUserClaim
    {
        /// 
        ///     Constructor
        /// 
        public IdentityUser()
        {
            Claims = new List();
            Roles = new List();
            Logins = new List();
        }

        /// 
        /// Email
        /// 
        public virtual string Email { get; set; }


Для реализации ограничения прав доступа в Identity предназначены специальные объекты — Роли. Роль в конфигурации может соответствовать должностям или видам деятельности различных групп пользователей.

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

    /// 
    /// EntityType that represents a user belonging to a role
    /// 
    /// 
    public class IdentityUserRole
    {
        /// 
        /// UserId for the user that is in the role
        /// 
        public virtual TKey UserId { get; set; }

        /// 
        /// RoleId for the role
        /// 
        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()
           .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()
                .HasKey(r => new { r.UserId, r.RoleId })
                .ToTable("AspNetUserRoles");

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

            modelBuilder.Entity()
                .ToTable("AspNetUserClaims");

            var role = modelBuilder.Entity()
                .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.

© Habrahabr.ru