С появлением технологии ASP.NET Identity от Microsoft .NET разработчики стали все чаще использовать ее при создании веб-приложений. Для краткого экскурса в технологию предлагаем прочитать статью. Эта технология присутствует в стандартном шаблоне проекта и позволяет использовать стандартную реализацию функциональности авторизации и аутентификации пользователя.

image


«Из коробки» провайдером данных для ASP.NET Identity является MSSQL, но поскольку система авторизация Identity может взаимодействовать с любой другой реляционной СУБД, мы исследовали и реализовали эту возможность для InterSystems Cache.

Во первых, для чего все это? Представим, что ваш проект использует СУБД Cache на .NET и вам потребовалась полноценная и надежная система авторизации. Писать такую систему с нуля руками крайне нецелесообразно, естественно, что вы захотите воспользоваться существующим аналогом в .NET — ASP.NET Identity. Но в чистом виде фреймворк способен работать только со своей нативной СУБД от Microsoft — MS SQL. Наша задача состояла в том, чтобы реализовать адаптер, который позволит легким движением руки портировать Identity на СУБД Intersystems Cache. Поставленная задача была реализована в ASP.NET Identity Cache Provider.
Суть проекта ASP.NET Identity Cache Provider заключается в имплиментации провайдера данных Cache для 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 (клеймы), который позволяет добавлять необходимые поля и потом использовать их, например, при регистрации пользователя.

Перейдем к рассмотрению реализации провайдера данных Cache для ASP.NET Identity. Она проходила в два этапа:

? Имплементация классов хранения данных (которые будут отвечать за хранение состояния) и класса IdentityDbContext, который инкапсулирует всю низкоуровневую логику работы с хранилищем данных. Также был имплементирован класс IdentityDbInitializer, который проводит адаптацию базы данных Cache для работы с 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 подготавливает базу данных Cache для использовани 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, которые инкапсулируют в себе логику добавления, редактирования и удаления пользователей, и ролей. Для этих сущностей требовалось стопроцентное покрытие юнит-тестами.

Подведем итоги: был реализован провайдер данных для работы СУБД Cache с Entity Framework в контексте технологии ASP.NET Identity. Приложение оформлено в отдельный Nuget-пакет, и теперь при необходимости работать с СУБД Cache, и при этом использовать стандартную авторизацию от Microsoft, достаточно просто внедрить сборку Identity Cache Provider в проект через Nuget Package Manager.

Реализация проекта с исходным кодом, примером и тестами выложена на GitHub.
Поделиться с друзьями
-->

Комментарии (14)


  1. ZOXEXIVO
    30.05.2016 16:03
    +2

    Все равно Identity неудобный, каким бы гибким он не был.
    Спасибо, что они хоть логику на хранимках не сделали, как в Membership


    1. morisson
      30.05.2016 16:35

      Т.е. не используете, а делаете с нуля?


      1. ZOXEXIVO
        30.05.2016 16:50
        +2

        В компании используем, но для своих проектов — всегда минимализм и производительность.


    1. Razaz
      30.05.2016 22:04

      MembershipReboot? Чем неудобен кстати?


  1. morisson
    30.05.2016 18:27

    А в какой версии Cache это у вас работает?


    1. TopCoder
      30.05.2016 18:54
      +1

      начиная с версии Cache 2015.2


  1. ProMayer
    30.05.2016 22:41

    Через поиск поиск студии 2015 не нашел нугет пакет, искал по ключевым словам Identity, Cache, Intersystems, не подскажите в чем дело? Надеюсь что заработает идентити с кеше из коробки.


  1. TopCoder
    30.05.2016 22:57

    На данный момент, пакет еще не залит в нугет, но вы можете воспользоваться той версией пакета, которая находится в репозитории на гитхабе


    1. tsafin
      30.05.2016 23:24
      +2

      Поддерживаю вопрос: если есть уже локальный nuget пакет, то что мешает запушить его в репозиторий Майкрософт?


      1. TopCoder
        30.05.2016 23:46
        +1

        Ничего не мешает, мы уже работаем над этим.


  1. nik0307
    30.05.2016 23:41

    Насколько я понял Вам пришлось адаптировать EF для работы СУБД Intersystems Cache. Как вообще EF нормально взаимодействует Cache? были какие-либо подводные камни?


    1. TopCoder
      30.05.2016 23:59

      Вполне нормально. В целом Cache является такой-же реляционной базой как и MS SQL. Каких-то особых трудностей с переносом EF на Cache не было. Делали замеры производительности, время отклика Cache в среднем в 1,5 раза быстрее, нежели у MS SQL.


  1. Konnor95
    30.05.2016 23:41

    Есть руководство по внедрению этого провайдера в существующий проект?


    1. TopCoder
      31.05.2016 00:01
      +1

      да, конечно — Руководство.