Аутентификация OWIN и ClaimsIdentity

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core

Последнее обновление: 16.11.2015

В этой статье рассмотрим, как мы можем аутентифицировать пользователя и разграничить доступ по ролям без всяких провайдеров и ASP.NET Identity средствами инфраструктуры OWIN. И для этого вначале создадим новый проект без аутентификации.

Прежде всего, нам необходимы модели пользователя и роли, а также контекст данных. Поэтому добавим в папку Models следующие классы:

public class User
{
    public int Id { get; set; }
	public string Email { get; set; }
    public string Password { get; set; }
	public Role Role { get; set; }
    public int RoleId { get; set; }
}
public class Role
{
    public int Id { get; set; }
	public string Name { get; set; }
}

И также добавим класс контекста данных и инициализатор:

public class UserContext : DbContext
{
    public UserContext():base("DefaultConnection")
    { }
    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }
}

public class UserDbInitializer : DropCreateDatabaseAlways<UserContext>
{
    protected override void Seed(UserContext db)
    {
        db.Roles.Add(new Role { Id = 1, Name = "admin" });
        db.Roles.Add(new Role { Id = 2, Name = "user" });
        db.Users.Add(new User
        {
            Email = "alice@gmail.com",
            Password = "123456",
            RoleId = 1
        });
        db.Users.Add(new User
        {
            Email = "tom@gmail.com",
            Password = "123456",
            RoleId = 2
        });
        base.Seed(db);
    }
}

Пусть в файле web.config у нас будет определена строка подключения DefaultConnection, а в файле Global.asax вызывается вышеопределенный инициализотор БД.

Теперь нам надо подключить OWIN в проект. Для этого добавим через Nuget следующие пакеты:

Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.Cookies

Они имеют ряд зависимостей, и вместе с ними будут добавлены пакеты OWIN, Microsoft.Owin и Microsoft.Owin.Security.

Затем добавим в проект в папку App_Start класс OWIN, который назовем Startup:

using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Security.Cookies;

[assembly: OwinStartup(typeof(IdentityUoWApp.Startup))]
namespace IdentityUoWApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "ApplicationCookie",
                LoginPath = new PathString("/Account/Login"),
            });
        }
    }
}

С помощью метода расширения UseCookieAuthentication() здесь устанавливается, что при аутентификации пользователей будут использоваться куки: AuthenticationType = "ApplicationCookie".

Теперь сделаем функционал для входа на сайт. Вначале добавим в проект папку ViewModels и в ней определим класс LoginViewModel - модель входа на сайт:

using System.ComponentModel.DataAnnotations;

public class LoginModel
{
    [Required]
    public string Email { get; set; }
    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }
}

Затем добавим в папку Controllers новый контроллер AccountController:

using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Data.Entity;
using System.Security.Claims;
using Microsoft.Owin.Security;
using IdentityUoWApp.ViewModels; // пространство имен LoginViewModel
using IdentityUoWApp.Models; // пространство имен моделей
using System.Threading.Tasks;

namespace IdentityUoWApp.Controllers
{
    public class AccountController : Controller
    {
        UserContext db = new UserContext();
        private IAuthenticationManager AuthenticationManager
        {
            get
            {
                return HttpContext.GetOwinContext().Authentication;
            }
        }

        public ActionResult Login()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginModel model)
        {
            if (ModelState.IsValid)
            {
                User user = await db.Users.Include(u=>u.Role).FirstOrDefaultAsync(u => u.Email == model.Email && u.Password == model.Password);
                
                if (user == null)
                {
                    ModelState.AddModelError("", "Неверный логин или пароль.");
                }
                else
                {
                    ClaimsIdentity claim = new ClaimsIdentity("ApplicationCookie", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
                    claim.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.String));
                    claim.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.Email, ClaimValueTypes.String));
                    claim.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", 
                        "OWIN Provider", ClaimValueTypes.String));
                    if (user.Role!=null)
                        claim.AddClaim(new Claim(ClaimsIdentity.DefaultRoleClaimType, user.Role.Name, ClaimValueTypes.String));

                    AuthenticationManager.SignOut();
                    AuthenticationManager.SignIn(new AuthenticationProperties
                    {
                        IsPersistent = true
                    }, claim);
                    return RedirectToAction("Index", "Home");
                }
            }
            return View(model);
        }

        public ActionResult Logout()
        {
            AuthenticationManager.SignOut();
            return RedirectToAction("Index", "Home");
        }
    }
}

Первым делом с помощью контекста OWIN мы получаем объект IAuthenticationManager. Что это за объект? Данный объект управляет аутентификационными куками и выполняет ряд других действий.

В POST-методе Login мы получаем из представления объект LoginModel и по нему определяем пользователя. Если пользователь был найден, то создается объект ClaimsIdentity:

ClaimsIdentity claim = new ClaimsIdentity("ApplicationCookie", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);

Первый параметр - "ApplicationCookie" указывате на тип аутентификации, то есть куки. И в конструктор также передаются константы ClaimsIdentity.DefaultNameClaimType и ClaimsIdentity.DefaultRoleClaimType, которые представляют название клейма логина пользователя и название клейма роли.

Далее в объект ClaimsIdentity последовательно добавляются клеймы, которые содержат логин пользователя, его id, роль:

claim.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.String));
claim.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.Email, ClaimValueTypes.String));
if (user.Role!=null)
    claim.AddClaim(new Claim(ClaimsIdentity.DefaultRoleClaimType, user.Role.Name, ClaimValueTypes.String));

И также нам надо добавить специальный клейм провайдера:

claim.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", 
    "OWIN Provider", ClaimValueTypes.String));

Причем второй параметр представляет в данном случае произвольное название провайдера - "OWIN Provider".

По похожей схеме происходит создание аутентификационного тикета в ASP.NET Identity, правда, там провайдер четко определен - "Asp Net Identity".

Далее с помощью методов AuthenticationManager.SignOut() и AuthenticationManager.SignIn() куки удаляются и вновь устанавливаются.

Таким образом, аутентификационный тикет будет содержать данные о логине пользователя, его роли и id. Мы уже по умолчанию можем получать логин пользователя: User.Identity.Name. Но на данный момент в проекте нет никакого способа определять id пользователя и роль. И для этого добавим в папку Models новый класс IdentityExtensions:

using System;
using System.Globalization;
using System.Security.Claims;
using System.Security.Principal;

namespace IdentityUoWApp.Models
{
    public static class IdentityExtensions
    {
        public static T GetUserId<T>(this IIdentity identity) where T : IConvertible
        {
            if (identity == null)
            {
                throw new ArgumentNullException("identity");
            }
            var ci = identity as ClaimsIdentity;
            if (ci != null)
            {
                var id = ci.FindFirst(ClaimTypes.NameIdentifier);
                if (id != null)
                {
                    return (T)Convert.ChangeType(id.Value, typeof(T), CultureInfo.InvariantCulture);
                }
            }
            return default(T);
        }
        public static string GetUserRole(this IIdentity identity)
        {
            if (identity == null)
            {
                throw new ArgumentNullException("identity");
            }
            var ci = identity as ClaimsIdentity;
            string role = "";
            if (ci != null)
            {
                var id = ci.FindFirst(ClaimsIdentity.DefaultRoleClaimType);
                if (id != null)
                    role = id.Value;
            }
            return role;
        }
    }
}

Данный класс создает два метода расширения GetUserId и GetUserRole для получения из аутентификационного тикета id и роли пользователя.

И мы можем, например, использовать разграничение входа и эти методы в имеющемся у нас HomeController:

using System.Web.Mvc;
using IdentityUoWApp.Models;

namespace IdentityUoWApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
        [Authorize(Roles ="admin")]
        public ActionResult About()
        {
            ViewBag.Message = User.Identity.GetUserRole();

            return View();
        }
        public ActionResult Contact()
        {
            int id = User.Identity.GetUserId<int>();
            ViewBag.Message = "Ваш id: " + id.ToString();

            return View();
        }
    }   
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850