Начало работы с ASP.NET Identity

Многие из вас должны знать, что выход ASP.NET MVC 5 ознаменовался переходом на новую систему авторизации под названием ASP.NET Identity. Разработчики фреймворка настоятельно рекомендуют переходить на новую систему, называя основными ее преимуществами возможность внедрения в абсолютно любой проект (ASP.NET MVC, Web Forms, Web Pages, Web API и SignalR), простую социальную интеграцию, работу на OWIN, установку и обновление посредством NuGet и другие. Присмотревшись внимательнее к ASP.NET Identity, можно без зазрения совести сказать, что это — следующий этап в развитии веб-программирования на ASP.NET. В данном посте я размещу простой туториал для начала работы с ASP.NET Identity.

Побродив немножко по интернету в поисках доступного туториала по реализации системы авторизации в проекте MVC 5, я нашел определенное количество тематических статей, но практически все они были ориентированы на использование Entity Framework как хранилища пользовательских данных. Даже корневой ресурс, к моему великому расстройству, не смог в должной степени раскрыть тему и ответить на все интересующие вопросы.

Пусть у нас имеется MVC-проект, где мы бы хотели использовать самые последние технологии, в частности Owin, Katana и конечно же ASP.NET Identity. Если вдруг термин Owin вводит вас в замешательство, рекомендую прочесть статью.

1. Основные библиотеки, которые нам потребуются: Microsoft.AspNet.Identity.Core, Microsoft.Owin, Microsoft.Owin.Security + все, от них зависимые и зависящие. Легко устанавливаются при помощи NuGet Package Manager. Через консоль NuGet это можно сделать так:

Install-Package Microsoft.AspNet.Identity.Core


2. Реализуем основные классы:
public class ApplicationUser : IUser
{
        public ApplicationUser(string name)
        {            
            Id = Guid.NewGuid().ToString();
            UserName = name;
        }

        public string Id { get; private set; }
        public string UserName { get; set; }
}

Интерфейс Microsoft.AspNet.Identity.IUser требует реализации полей Id и UserName. Помимо этого вы можете добавить нужные вам поля (Email, Password, City и т.д.)

public class CustomUserStore : IUserStore<ApplicationUser>
{
        static readonly List<ApplicationUser> Users = new List<ApplicationUser>();

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public Task CreateAsync(ApplicationUser user)
        {
            return Task.Factory.StartNew(() => Users.Add(user));
        }

        public Task UpdateAsync(ApplicationUser user)
        {
            throw new NotImplementedException();
        }

        public Task DeleteAsync(ApplicationUser user)
        {
            throw new NotImplementedException();
        }

        public Task<ApplicationUser> FindByIdAsync(string userId)
        {
            throw new NotImplementedException();
        }

        public Task<ApplicationUser> FindByNameAsync(string userName)
        {
            return Task<ApplicationUser>.Factory.StartNew(() => Users.FirstOrDefault(u => u.UserName == userName));
        }
}

CustomUserStore, как понятно из названия, является хранилищем пользователей и включает базовые методы (Create, Update, Delete) для работы с ними. Здесь я использую для хранения статическое поле Users. Здесь же можно прикрутить любое, подходящее для вас хранилище. Также нельзя не обратить внимание на тип возвращаемого значения — Task и Task. Это означает, что методы будут выполняться асинхронно. По этой теме есть неплохие материалы на хабре (например, вот).

public class CustomUserManager : UserManager<ApplicationUser>
{
        public CustomUserManager(CustomUserStore store)
            : base(store)
        {
                this.PasswordHasher = new CustomPasswordHasher();
        }

        public override Task<ApplicationUser> FindAsync(string userName, string password)
        {
            Task<ApplicationUser> taskInvoke = Task<ApplicationUser>.Factory.StartNew(() =>
            {               
                PasswordVerificationResult result = this.PasswordHasher.VerifyHashedPassword(userName, password);
                if (result == PasswordVerificationResult.SuccessRehashNeeded)
                {    
                    return Store.FindByNameAsync(userName).Result;
                }
                return null;
            });
            return taskInvoke;
        }
}

public class CustomPasswordHasher : PasswordHasher
{
        public override string HashPassword(string password)
        {
            return base.HashPassword(password);
        }

        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {           
            if (true)
            {
                return PasswordVerificationResult.SuccessRehashNeeded;
            }
            else
            {
                return PasswordVerificationResult.Failed;
            }
        }
}

CustomUserManager — главный класс, методы которого мы будем вызывать для манипулирования пользовательскими данными(замена старому Membership Provider). Он должен являться наследником Microsoft.AspNet.Identity.UserManager, который содержит множество виртуальных методов. В данном случае мы переопределили метод FindAsync(), в котором для примера сначала проверяется пароль, и в случае успеха возвращается юзер. Как вы видите, в конструкторе UserManeger мы определили собственный PasswordHasher — класс, предназначенный для управления паролями. Тело метода VerifyHashedPassword имеет демонстрационный вид. Вы же можете там написать свою логику проверки пароля.

3. Настраиваем приложение на работу с нашим CustomUserManager. Для этого в файле Startup.cs, который исполняется сборкой Owin при запуске приложения, добавляем строку:

        app.CreatePerOwinContext(CustomUserManager.Create);


4. Используем CustomUserManager внутри контроллера:

public class TestController : ApiController
{
        private static CustomUserManager _customUserManager;

        public CustomUserManager UserManager
        {
            get
            {
                return _customUserManager ??
                       (_customUserManager = HttpContext.Current.GetOwinContext().GetUserManager<CustomUserManager>());
            }
        }
        public async Task<bool> Authenticate(string name, string password)
        {
           if (await UserManager.FindAsync(name, password) != null)
            {
                return true;
            }

            return false;
        }
}


Вы можете использовать те методы UserManager, которые вы переопределили, а также стандартный набор методов класса CustomUserStore (они вызываются по умолчанию, если вы используете одноименные методы вашего UserManager), которые должны быть определены в обязательном порядке.

5. Для использования стандартных атрибутов MVC [Authorize] установите нужный вам тип авторизации в классе Startup. Например:
public class Startup
{
        public void Configuration(IAppBuilder app)
        {
                app.UseCookieAuthentication(new CookieAuthenticationOptions
                {
                        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                        LoginPath = new PathString("/Account/Login")
                });
        }
}


Список использованных ресурсов:
Simple Asp.net Identity Core Without Entity Framework
ASP.NET Identity 2.0 Cookie & Token Authentication

P.S. Данный материал является дебютным и, конечно же, не претендует на исчерпывающий мануал по ASP.NET Identity. Рассчитан на людей, знакомых с технологией MVC, желающих познакомиться с новой системой авторизации.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 18
  • 0
    я нашел определенное количество тематических статей, но практически все они были ориентированы на использование Entity Framework как хранилища пользовательских данных.

    EE — это действительно проблема. В одном проекте была построена инфраструктура на основе NHibernate — я так и не осилил «красивое» внедрение ASP.NET Identity. Пришлось костыли использовать.
    • +1
      «Красивое» — это в смысле такое же как в примерах по EF?
      По-моему, можно вполне себе красиво реализовать свой UserStore, как например вот здесь, используя еще один важный класс DbContext, который, к сожалению не вошел в мой стартовый туториал.
      • 0
        Хм, реализация своего UserStore выглядит здорово. Скоро буду переделывать как раз таки этот участок проекта, попробую заново внедрить ASP.NET Identity. Спасибо за ссылку.
    • 0
      так сказать в копилку, реализация доп. провайдеров типа LinkedIn, Yahoo, GitHub… www.nuget.org/packages/Owin.Security.Providers/
      только обратите внимание: LinkedIn недавно стал требовать обязательного указания Redirect_Url, раньше они это поле игнорировали.
      • 0
        Возможно, кто-нибудь сталкивался со следующей проблемой и знает как её решить:

        Итак, в проекте используется связка EF + ASP.NET Identity. Как выше было уже отмечено, во главу угла в ASP.NET Identity поставлена асинхронность, т.е. почти каждый метод в этом фреймворке(каркасе?) возвращает объект типа Task. А это значит, что запрос начавшись в одном потоке, возможно закончит свое выполнение в другом.
        Также известно, что EF не является потокобезопасным, более того, как только какой-нибудь поток пытается получить данные из контекста созданного в другом потоке, EF немедленно генерирует NotSupportedException с примерно следующим описанием:

        «A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.»

        Вот здесь и здесь можно найти подробное обсуждение этой проблемы. В общем народ спихивает ответственность на AspNetSynchronizationContext.
        Это звучит разумно и логично, однако мне не ясно следующее. Как работать с ASP.NET Identity + EF если у него есть такая проблема(например невозможно сделать два await вызова к одному контексту)?

        Есть вероятность, что я упускаю, какое-то соверешенно очевидно решение, так как я не верю что парни из MS не думали\сталкивались с этой проблемой.
        • 0
          Это звучит разумно и логично, однако мне не ясно следующее. Как работать с ASP.NET Identity + EF если у него есть такая проблема(например невозможно сделать два await вызова к одному контексту)?


          Сделать два разных контекста? Вообще хорошая практика DbContext per Request, с подсовыванием с помощью IoC.
          • 0
            В этом случае надо разделять понятия веб-запрос и запрос данных к контексту.

            Представим такую ситуацию, к нам приходит запрос(веб-запрос) от клиента. Наш замечательный DI-контейнер создает экзепляр класса DbContext. После этого нам надо выполнить два действия:
            1. Получить информацию о клиенте(например авторизационную информацию)
            2. Получить какие-нибудь ещё данные специфичные для запроса

            Таким образом к нашему объекту класса DbContext, нужно будет сделать два запроса данных. И тут появляется проблема, если эти запросы выполняются не одним и тем же потоком.

            Есть другой вариант, создавать объект класса DbContext на каждый запрос данных. Я не проводил замеры производительности, но интуитивно чувствую, что этот подход будет иметь сильное негативное влияние на быстродействие.
            • +1
              Только что написал пример:
                      public async Task<ActionResult> Index()
                      {
              
                          var dbContext = new TestEntites();
                          var something = await dbContext.Foo.FirstOrDefaultAsync(e => e.Id == 1);
                          var morething = await dbContext.Foo.FirstOrDefaultAsync(e => e.Id == 2);
              
                          return View();
                      }
              

              Естественно работает без ошибок.
              • 0
                А вы не могли бы проверить, сколько потоков выполняют ваш код?

                Например таким образом:
                        public async Task<JsonResult> Index()
                        {
                            int initialThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
                            var dbContext = new TestEntites();
                
                            var something = await dbContext.Foo.FirstOrDefaultAsync(e => e.Id == 1);
                            int afterFirstAwaitThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
                
                            var morething = await dbContext.Foo.FirstOrDefaultAsync(e => e.Id == 2);
                            int afterSecondAwaitThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
                
                            return Json(new { initial  = initialThreadId, first = afterFirstAwaitThreadId, second = afterSecondAwaitThreadId });
                        }
                


                Вполне может такое случится, что ваш запрос выполняет один и тот же поток, особенно если вы запускали код с прикрепленным отладчиком.
                • 0
                  Поток один, как с отладчиком, так и без. При этом в callstack видно что это честное асинхронное продолжение
                  Ты думаешь что для такого кейса не запилили бы стабильную реализацию?
                  • 0
                    Я уверен, что её либо не запили вообще, либо это сделали не полностью. Это легко проверить, ASP.NET для создания и выполнения Task'ок использует AspNetSynchronizationContext, который в свою очередь использует ThreadPool для управления потоками.
                    Можно попробовать создать много запросов к приложению, и ждать когда продолжение(continuation) веб-запроса будет выполняться не потоком инициатором запроса. И вот тогда будет проблема(я с ней столкнулся). Опять-таки это проблема EF, а не проблема ASP.NET, так как EF не потокобезопасен.
                    Если интересно вникнуть чуть глубже, рекомендую посмотреть эти два вопроса и особенно ответы к ним на SO:
                    1. stackoverflow.com/questions/20946677/ef-data-context-async-await-multithreading
                    2. stackoverflow.com/questions/20993007/how-to-use-non-thread-safe-async-await-apis-and-patterns-with-asp-net-web-api

                    Во всей этой истории меня удивляет следующее, почему авторы ASP.NET Identity не предоставили синхронного API? Я понимаю, что проблем не должно быть со всякими MongoDB, потому как они асинхронны. Я понимаю, что проблемы нет в WPF и WinForms, так как они используют другой синхронизационный контекст. НО ё-маё, проблема есть в довольно часто используемой связке ASP.NET MVC + EF, когда сюда добавляем ASP.NET Identity

                    • 0
                      Я уверен, что её либо не запили вообще, либо это сделали не полностью.

                      :) Откуда такая уверенность?

                      Это легко проверить, ASP.NET для создания и выполнения Task'ок использует AspNetSynchronizationContext, который в свою очередь использует ThreadPool для управления потоками.
                      Можно попробовать создать много запросов к приложению, и ждать когда продолжение(continuation) веб-запроса будет выполняться не потоком инициатором запроса. И вот тогда будет проблема(я с ней столкнулся). Опять-таки это проблема EF, а не проблема ASP.NET, так как EF не потокобезопасен.

                      Запустил LoadTest, 500 запросов в секунду, ошибок нет. Так что нет проблем у EF и ASP.NET (в том числе MVC и WebAPI), по крайней мере тех, о которых вы думаете.
                      • 0
                        Проблема есть не только у меня, о чем свидетельствует SO.
                        Я постараюсь сегодня или завтра написать пример стабильно воспроизодящий проблему.
                        • 0
                          Проблема есть не только у меня, о чем свидетельствует SO.

                          не читайте до обеда советских газет

                          Пример будет интересен, только пожалуйста без экзотики.
                          • 0
                            Пример нашелся?
                            • 0
                              Прошу прощения за задержку, пока не смог его дистиллировать из своего проекта, как только сделаю это обязательно отпишусь в эту ветку.
          • +2
            Кстати, есть замечательный цикл статей, который описывает структуру и архитектуру ASP.NET Identity, рекомендую ознакомится тем, кто только начинает работу с этим framework'ом. Плюс, как бонус, даются ссылки на реализации бибилотек с различными хранилищами, такими как RavenDB, MongoDB и т.д.
            • +1
              Добавлю:

              в консольке пишем «Install-Package Microsoft.AspNet.Identity.Samples -Pre» и нам в солюшен приезжает готовый рабочий семпл, с комментариями, EF в качестве хранилища и вёрсткой.

              Найдено тут: www.codeproject.com/Articles/762428/ASP-NET-MVC-and-Identity-Understanding-the-Basics

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.