Руководство ASP.NET Identity Framework 2.0

История развития авторизации в ASP.NET

ASP.NET Membership

ASP.NET Membership создан в 2005, чтобы позволить пользователю проходить авторизацию с помощью проверки подлинности на основе формы данных. Использовался исключительно SQL Server. Для работы при использовании такого типа авторизации создавались дополнительно хранимые процедуры для каждой базы данных. При использовании авторизации этого типа были следующие сложности:

Simple Membership

Simple Membership был добавлен вместе с WebMatrix в Visual Studio 2010 SP1. Целью введения этой системы авторизации служило простое добавление модуля авторизации в сайт под управлением ASP.NET. Примерно год назад я писал подробнее об использовании этой системы в ASP.NET MVC 4. Скажу честно, у меня эта система не прижилась. Было довольно трудно создавать приложение и интегрировать эту систему. Тем более в существующие сайты встроить ее было сложно (и сегодня практически ничего не изменилось). Чаще всего я использовал свой велосипед (на 90% сайтов). Подробнее о нем.

Введение

До релиза версии Asp.Net Identity Framework 2.0 использовалась 1-я версия этого фреймворка. Я пропустил этот этап и начал знакомство со второй (к счастью для меня). Шаблон с этой системой авторизации используется в Visual Studio 2013 для новых проектов MVC (WebAPI и др.)

Основные преимущества использования ASP.NET Identity Framework 2.0

Начало работы

Шаблонный проект создаст за нас все необходимые классы, дополнения и т. д. Поэтому, для того, чтобы понять как на самом деле используется ASP.NET Identity Framework мы рассмотрим процесс создания с нуля. Начнем с чистого листа (ASP.NET MVC)

Новый проект asp.net mvc 5 empty

Создаем пустой проект

create empty project

Подключаем необходимые пакеты Nuget

Для разработки приложения asp.net mvc по спецификации owin требуется довольно много пакетов. Как перенести или скопировать nuget пакеты в другой проект я описывал ранее. Для себя я выбрал примерно такие:

<?xml version=“1.0” encoding=“utf-8”?>
<packages>
  <package id=“Antlr” version=“3.4.1.9004” targetFramework=“net451” />
  <package id=“bootstrap” version=“3.0.0” targetFramework=“net451” />
  <package id=“CommonServiceLocator” version=“1.3” targetFramework=“net451” />
  <package id=“EntityFramework” version=“6.1.0” targetFramework=“net451” />
  <package id=“jQuery” version=“1.9.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.Identity.Core” version=“2.0.1” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.Identity.Core.ru” version=“2.0.1” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.Identity.EntityFramework” version=“2.0.1” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.Identity.Owin” version=“2.0.1” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.Mvc” version=“5.0.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.Razor” version=“3.0.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.Web.Optimization” version=“1.1.3” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.WebApi” version=“5.2.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.WebApi.Client” version=“5.2.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.WebApi.Core” version=“5.2.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.WebApi.Owin” version=“5.2.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.WebApi.OwinSelfHost” version=“5.2.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.WebApi.WebHost” version=“5.2.0” targetFramework=“net451” />
  <package id=“Microsoft.AspNet.WebPages” version=“3.0.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin.Diagnostics” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin.Host.HttpListener” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin.Host.SystemWeb” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin.Hosting” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin.Security” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin.Security.Cookies” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Owin.Security.OAuth” version=“2.1.0” targetFramework=“net451” />
  <package id=“Microsoft.Web.Infrastructure” version=“1.0.0.0” targetFramework=“net451” />
  <package id=“Modernizr” version=“2.6.2” targetFramework=“net451” />
  <package id=“Newtonsoft.Json” version=“5.0.4” targetFramework=“net451” />
  <package id=“Owin” version=“1.0” targetFramework=“net451” />
  <package id=“Oxozle.Utilities” version=“1.1” targetFramework=“net451” />
  <package id=“Oxozle.Utilities.Web” version=“1.1.2” targetFramework=“net451” />
  <package id=“structuremap” version=“3.0.0.108” targetFramework=“net451” />
  <package id=“StructureMap.MVC5” version=“3.0.4.125” targetFramework=“net451” />
  <package id=“structuremap.web” version=“3.0.0.108” targetFramework=“net451” />
  <package id=“WebActivatorEx” version=“2.0.5” targetFramework=“net451” />
  <package id=“WebGrease” version=“1.5.2” targetFramework=“net451” />
</packages>

Эти пакеты нужны для использования IoC, WebApi, EntityFramework и IdentityFramework в приложении.

База данных

При создании проекта из шаблонов Visual Studio создает локальную базу данных. Для того, чтобы интегрировать систему авторизации в существующий проект или в пустой проект необходимо добыть скрипты создания базы данных. Необходимо обратить внимание на 2 пункта:

  1. По умолчанию использование Identity Framework предполагает использование Code First. В силу некоторых причин мне не нравится использование такого подхода. Т.к. хочется полностью контролировать все изменения в базе данных на уровне SQL Server;
  2. Опять же по умолчанию IdentityFramework предполагает использование varchar в качестве ключа в таблицах.

2-я версия набора библиотек построена на основе generic типов. Это означает, что теоритически можно изменить тип ключа. Остается решить вопрос как это сделать в 2-х местах: в базе данных (так, чтобы было удобно ее разворачивать) и в коде. Для решения второго пункта очень сильно помогла статья ASP.NET Identity 2.0 Change Primary Key from String to Integer. А первая проблема легко не сдавалась. Осложняло решение еще и то, что CodeFirst пользуется популярностью. Я перепробовал много способов генерации таблиц и базы данных (ссылки в конце статьи).

Пришлось разобраться с шаблоном проекта DataBase в Visual Studio и сделать свой проект базы данных. Речь идет о создании 5-ти таблиц. Для удобства я добавил префикс AspNet перед названиями таблицы. Так можно быстро определить таблицы для работы с пользователями. Уже созданные таблицы в базе данных:

asp.net identity framework tables

И их структурный вид

таблицы для авторизации asp.net mvc

Создаем проект SQL Server DataBase Project (либо скачиваем готовый проект, ссылка в конце статьи).

sql server database project

И создаем таблицы.

sql tables asp.net identity framework

Скачать таблицы или целый проект.

Публикация базы данных

Доступ из кода

После того, как база данных создана необходимо найти решение второй озвученной ранее проблемы, а именно первичные ключи в каждой таблице.

В своих проектах я принял некоторое соглашение, что для asp.net identity framework всегда будет отдельная запись в web.config в секции настроек строк подключения к базе данных. И называться она будет IdentityConnection.

Выбрав EntityFramework как ORM для доступа к данным необходимо реализовать соответствующие классы. Напомню, мы выбрали в качестве первичного ключа в таблице Int. Рассмотрим наиболее инетерсные классы в такой схеме.

ApplicationDbContext

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, RoleIntPk, int,
    UserLoginIntPk, UserRoleIntPk, UserClaimIntPk>
{
    public ApplicationDbContext()
        : base(“IdentityConnection”)
    {
    }

public static ApplicationDbContext Create()
{
    return new ApplicationDbContext();
}

public void Initialize()
{
    var manager = ApplicationUserManager.CreateStatic(this);

    // Логика для инициализации пользователей
    // Создании системных пользователей
    // И стандартных ролей
}

}

ApplicationUserManager

public class ApplicationUserManager : UserManager<ApplicationUser, int>
{
    public ApplicationUserManager(IUserStore<ApplicationUser, int> store)
        : base(store)
    {
    }

public static UserStoreIntPk APPCreateUserStore(IOwinContext context)
{
    return new UserStoreIntPk(context.Get&lt;ApplicationDbContext&gt;());
}


public static ApplicationUserManager CreateStatic(ApplicationDbContext context)
{
    ApplicationUserManager manager = new ApplicationUserManager(new UserStoreIntPk(context));

    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator&lt;ApplicationUser, int&gt;(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true,
    };

    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 1,
    };

    return manager;
}



public static ApplicationUserManager Create(IdentityFactoryOptions&lt;ApplicationUserManager&gt; options,
    IOwinContext context)
{
    ApplicationUserManager manager = new ApplicationUserManager(new UserStoreIntPk(context.Get&lt;ApplicationDbContext&gt;()));
    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator&lt;ApplicationUser, int&gt;(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true,
    };

    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 1,
    };

    var dataProtectionProvider = options.DataProtectionProvider;
    if (dataProtectionProvider != null)
    {
        manager.UserTokenProvider = new DataProtectorTokenProvider&lt;ApplicationUser, int&gt;
                                    (dataProtectionProvider.Create("ASP.NET Identity"));
    }
    return manager;
}

}

Также, хотелось бы из кода иметь доступ к числовому идентификатору пользователя. Для этого сделаем метод расширение для интерфейса IIdentity.

public static int GetUserIdIntPk(this IIdentity user)
{
    return Convert.ToInt32(user.GetUserId());

}

Скачать проект Oxozle.IdentityFramework.Models.zip.

Настройка Asp.Net проекта OWIN

Добавим несколько настроек в Startup.cs.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

    //Дополнительная логика настройки
}

}

Также рассмотрим процедуру авторизации в AccountController.

public class AccountController : AppControllerBase
{
    private ApplicationUserManager _userManager;
    public ApplicationUserManager UserManager
    {
        get
        {
            if (_userManager == null)
                _userManager = System.Web.HttpContext.Current.Request.GetOwinContext().GetUserManager<ApplicationUserManager>();

        return _userManager;

    }
}

private IAuthenticationManager AuthenticationManager
{
    get
    {
        return HttpContext.GetOwinContext().Authentication;
    }
}

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        ApplicationUser user = UserManager.Find(model.Email, model.Password);
        if (user != null)
        {
            SignInAsync(user, model.RememberMe);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "Неправильный email или пароль.");
        }
    }
    else
    {
        ModelState.AddModelError("", "Введите email и пароль");
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}



[HttpGet]
[AllowAnonymous]
public ActionResult Login()
{
    return View();
}

private void SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent },
        user.GenerateUserIdentity(UserManager));
}

private ActionResult RedirectToLocal(string returnUrl)
{
    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    else
    {
        return RedirectToAction("Index", "Site");
    }
}

}

Поиск пользователей по имени или email

Бонусом рассмотрим как искать пользователей по имени или email. Для этого допишем небольшое расширение (источник).

public static ApplicationUser FindByNameOrEmail
   (this ApplicationUserManager userManager, string usernameOrEmail, string password)
{
    var username = usernameOrEmail;
    if (usernameOrEmail.Contains(“@”))
    {
        var userForEmail = userManager.FindByEmail(usernameOrEmail);
        if (userForEmail != null)
        {
            username = userForEmail.UserName;
        }
    }
    return userManager.Find(username, password);
}

Ссылки

Скачать

Комментарии

comments powered by Disqus