Пользователь
0,0
рейтинг
9 апреля 2013 в 22:55

Разработка → ASP.NET MVC Урок 6. Авторизация tutorial

ASP*, .NET*
Цель урока: Изучить способ авторизации через Cookie, использование стандартных атрибутов доступа к контроллеру и методу контроллера. Использование IPrincipal. Создание собственного модуля (IHttpModule) и собственного фильтра IActionFilter.

Небольшое отступление: На самом деле в asp.net mvc все учебники рекомендуют пользоваться уже придуманной системой авторизации, которая называется AspNetMembershipProvider, она была описана в статье http://habrahabr.ru/post/142711/ (сейчас доступ уже закрыт), но обьяснено это с точки зрения «нажимай и не понимай, что там внутри». При первом знакомстве с asp.net mvc меня это смутило. Далее, в этой статье http://habrahabr.ru/post/143024/ — сказано, что пользоваться этим провайдером – нельзя. И я согласен с этим. Здесь же, мы достаточно глубоко изучаем всякие хитрые asp.net mvc стандартные приемы, так что это один из основных уроков.


Кукисы

Кукисы – это часть информации, отсылаемая сервером браузеру, которую браузер возвращает обратно серверу вместе с каждым (почти каждым) запросом.

Сервер в заголовок ответа пишет:
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]

Например:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Set-Cookie: name2=value2; Expires=Wed, 09-Jun-2021 10:18:14 GMT

Браузер (если не истекло время действия кукиса) при каждом запросе:
GET /spec.html HTTP/1.1
Host: www.example.org
Cookie: name=value; name2=value2
Accept: */*


Устанавливаем cookie (/Areas/Default/Controllers/HomeController.cs):
public ActionResult Index()
        {
            var cookie = new HttpCookie() 
            {
                Name ="test_cookie", 
                Value = DateTime.Now.ToString("dd.MM.yyyy"),
                Expires = DateTime.Now.AddMinutes(10),
            };
            Response.SetCookie(cookie);
            return View();
        }


В Chrome проверяем установку:



Для получения кукисов:
var cookie = Request.Cookies["test_cookie"];


Делаем точку остановки и проверяем:



Примечание: подробнее можно изучить кукисы по следующей ссылке:
http://www.nczonline.net/blog/2009/05/05/http-cookies-explained/


Авторизация

В нашем случае авторизация будет основана на использовании кукисов. Для этого изучим следующие положения:
  • FormsAuthenticationTicket – мы воспользуемся этим классом, чтобы хранить данные авторизации в зашифрованном виде
  • Нужно реализовать интерфейс IPrincipal и установить в HttpContext.User для проверки ролей и IIdentity интерфейса.
  • Для интерфейса IIdentity сделать реализацию
  • Вывести в BaseController в свойство CurrentUser значение пользователя, который сейчас залогинен.


Приступим.
Создадим интерфейс IAuthentication и его реализацию CustomAuthentication (/Global/Auth/IAuthentication.cs):

public interface IAuthentication
    {
        /// <summary>
        /// Конекст (тут мы получаем доступ к запросу и кукисам)
        /// </summary>
        HttpContext HttpContext { get; set; }

        User Login(string login, string password, bool isPersistent);

        User Login(string login);

        void LogOut();

        IPrincipal CurrentUser { get; }
    }


Реализация (/Global/Auth/CustomAuthentication.cs):
    public class CustomAuthentication : IAuthentication
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        private const string cookieName = "__AUTH_COOKIE";

        public HttpContext HttpContext { get; set; }

        [Inject]
        public IRepository Repository { get; set; }

        #region IAuthentication Members

        public User Login(string userName, string Password, bool isPersistent)
        {
            User retUser = Repository.Login(userName, Password);
            if (retUser != null)
            {
                CreateCookie(userName, isPersistent);
            }
            return retUser;
        }

        public User Login(string userName)
        {
            User retUser = Repository.Users.FirstOrDefault(p => string.Compare(p.Email, userName, true) == 0);
            if (retUser != null)
            {
                CreateCookie(userName);
            }
            return retUser;
        }

        private void CreateCookie(string userName, bool isPersistent = false)
        {
            var ticket = new FormsAuthenticationTicket(
                  1,
                  userName,
                  DateTime.Now,
                  DateTime.Now.Add(FormsAuthentication.Timeout),
                  isPersistent,
                  string.Empty,
                  FormsAuthentication.FormsCookiePath);

            // Encrypt the ticket.
            var encTicket = FormsAuthentication.Encrypt(ticket);

            // Create the cookie.
            var AuthCookie = new HttpCookie(cookieName)
            {
                Value = encTicket,
                Expires = DateTime.Now.Add(FormsAuthentication.Timeout)
            };
            HttpContext.Response.Cookies.Set(AuthCookie);
        }

        public void LogOut()
        {
            var httpCookie = HttpContext.Response.Cookies[cookieName];
            if (httpCookie != null)
            {
                httpCookie.Value = string.Empty;
            }
        }

        private IPrincipal _currentUser;

        public IPrincipal CurrentUser
        {
            get
            {
                if (_currentUser == null)
                {
                    try
                    {
                        HttpCookie authCookie = HttpContext.Request.Cookies.Get(cookieName);
                        if (authCookie != null && !string.IsNullOrEmpty(authCookie.Value))
                        {
                            var ticket = FormsAuthentication.Decrypt(authCookie.Value);
                            _currentUser = new UserProvider(ticket.Name, Repository);
                        }
                        else
                        {
                            _currentUser = new UserProvider(null, null);
                        }
                    }
                    catch (Exception ex)
                    {
                        logger.Error("Failed authentication: " + ex.Message);
                        _currentUser = new UserProvider(null, null);
                    }
                }
                return _currentUser;
            }
        }
        #endregion
    }



Суть сводится к следующему, мы, при инициализации запроса, получаем доступ к HttpContext.Request.Cookies и инициализируем UserProvider:

var ticket = FormsAuthentication.Decrypt(authCookie.Value);
_currentUser = new UserProvider(ticket.Name, Repository);


Для авторизации в IRepository добавлен новый метод IRepository.Login. Реализация в SqlRepository:
  public User Login(string email, string password)
        {
            return Db.Users.FirstOrDefault(p => string.Compare(p.Email, email, true) == 0 && p.Password == password);
        }


UserProvider, собственно, реализует интерфейс IPrincipal (в котором есть проверка ролей и доступ к IIdentity).
Рассмотрим класс UserProvider (/Global/Auth/UserProvider.cs):

public class UserProvider : IPrincipal
    {
        private UserIndentity userIdentity { get; set; }

        #region IPrincipal Members

        public IIdentity Identity
        {
            get
            {
                return userIdentity;
            }
        }

        public bool IsInRole(string role)
        {
            if (userIdentity.User == null)
            {
                return false;
            }
            return userIdentity.User.InRoles(role);
        }

        #endregion

        
        public UserProvider(string name, IRepository repository)
        {
            userIdentity = new UserIndentity();
            userIdentity.Init(name, repository);
        }


        public override string ToString()
        {
            return userIdentity.Name;
        }



Наш UserProvider знает про то, что его IIdentity классом есть UserIdentity, а поэтому знает про класс User, внутри которого мы реализуем метод InRoles(role):

public bool InRoles(string roles)
        {
            if (string.IsNullOrWhiteSpace(roles))
            {
                return false;
            }

var rolesArray = roles.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var role in rolesArray)
            {
var hasRole = UserRoles.Any(p => string.Compare(p.Role.Code, role, true) == 0);
                if (hasRole)
                {
                    return true;
                }
            }
            return false;
        }


В метод InRoles мы ожидаем, что придет запрос о ролях, которые допущены к ресурсу, разделенные запятой. Т.е., например, “admin,moderator,editor”, если хотя бы одна из ролей есть у нашего User – то возвращаем зачение «истина» (доступ есть). Сравниваем по полю Role.Code, а не Role.Name.
Рассмотрим класс UserIdentity (/Global/Auth/UserIdentity.cs):
 public class UserIndentity : IIdentity
    {
        public User User { get; set; }

        public string AuthenticationType
        {
            get
            {
                return typeof(User).ToString();
            }
        }

        public bool IsAuthenticated
        {
            get
            {
                return User != null;
            }
        }

        public string Name
        {
            get
            {
                if (User != null)
                {
                    return User.Email;
                }
                //иначе аноним
                return "anonym";
            }
        }

        public void Init(string email, IRepository repository)
        {
            if (!string.IsNullOrEmpty(email))
            {
                User = repository.GetUser(email);
            }
        }
    }

В IRepository добавляем новый метод GetUser(email). Реализация для SqlRepository.GetUser() (LessonProject.Model:/SqlRepository/User.cs):

   public User GetUser(string email)
        {
return Db.Users.FirstOrDefault(p => string.Compare(p.Email, email, true) == 0);
        }


Почти все готово. Выведем CurrentUser в BaseController:
[Inject]
        public IAuthentication Auth { get; set; }
public User CurrentUser
        {
            get
            {
                return ((UserIndentity)Auth.CurrentUser.Identity).User;
            }
        }


Да, это не очень правильно, так как здесь присутствует сильное связывание. Поэтому сделаем так, введем еще один интерфейс IUserProvider, из которого мы будем требовать вернуть нам авторизованного User:
public interface IUserProvider
    {
        User User { get; set; }
    }
…
public class UserIndentity : IIdentity, IUserProvider
    {
        /// <summary>
        /// Текщий пользователь
        /// </summary>
        public User User { get; set; }
…
[Inject]
public IAuthentication Auth { get; set; }

public User CurrentUser
{
    get
    {
        return ((IUserProvider)Auth.CurrentUser.Identity).User;
    }
}

А теперь попробуем инициализировать это всё.
Вначале добавим наш IAuthentication + CustomAuthentication в регистрацию к Ninject (/App_Start/NinjectWebCommon.cs):

kernel.Bind<IAuthentication>().To<CustomAuthentication>().InRequestScope();


Потом создадим модуль, который будет на событие AuthenticateRequest совершать действие авторизации:
public class AuthHttpModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.AuthenticateRequest += new EventHandler(this.Authenticate);
        }

        private void Authenticate(Object source, EventArgs e)
        {
            HttpApplication app = (HttpApplication)source;
            HttpContext context = app.Context;

            var auth = DependencyResolver.Current.GetService<IAuthentication>();
            auth.HttpContext = context;
     context.User = auth.CurrentUser;
        }

        public void Dispose()
        {
        }
    }



Вся соль в строках: auth.HttpContext = context и context.User = auth.CurrentUser. Как только наш модуль авторизации узнает о контексте и содержащихся в нем кукисах, ту же моментально получает доступ к имени, по нему он в репозитории получает данныепользователя и возвращает в BaseController. Но не сразу всё, а по требованию.
Подключаем модуль в Web.config:
 <system.web>
…
    <httpModules>
      <add name="AuthHttpModule" type="LessonProject.Global.Auth.AuthHttpModule"/>
    </httpModules>
</system.web>


План таков:
  • Наверху показываем, авторизован пользователь или нет. Если авторизован, то его email и ссылка на выход, если нет, то ссылки на вход и регистрацию
  • Создаем форму для входа
  • Если пользователь правильно ввел данные – то авторизуем его и отправляем на главную страницу
  • Если пользователь выходит – то убиваем его авторизацию


Поехали. Добавляем Html.Action(“UserLogin”, “Home”) – это partial view (т.е. кусок кода, который не имеет Layout) – т.е. выводится где прописан, а не в RenderBody().
_Layout.cshtml (/Areas/Default/Views/Shared/_Layout.cshtml):

<body>
<div class="navbar navbar-fixed-top">
        <div class="navbar-inner">
            <div class="container">
                <ul class="nav nav-pills pull-right">
                    @Html.Action("UserLogin", "Home")
                </ul>
            </div>
        </div>
    </div>

    @RenderBody()

HomeController.cs:
public ActionResult UserLogin()
        {
            return View(CurrentUser);
        }



UserLogin.cshtml (/Areas/Default/Views/Home/UserLogin.cshtml):

@model LessonProject.Model.User

@if (Model != null)
{
    <li>@Model.Email</li>
    <li>@Html.ActionLink("Выход", "Logout", "Login")</li>
}
else
{
    <li>@Html.ActionLink("Вход", "Index", "Login")</li>
    <li>@Html.ActionLink("Регистрация", "Register", "User")</li>
}


Контроллер входа выхода LoginController (/Areas/Default/Controllers/LoginController.cs):

public class LoginController : DefaultController
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(new LoginView());
        }

        [HttpPost]
        public ActionResult Index(LoginView loginView)
        {
            if (ModelState.IsValid)
            {
var user = Auth.Login(loginView.Email, loginView.Password, loginView.IsPersistent);
                if (user != null)
                {
                    return RedirectToAction("Index", "Home");
                }
                ModelState["Password"].Errors.Add("Пароли не совпадают");
            }
            return View(loginView);
        }

        public ActionResult Logout()
        {
            Auth.LogOut();
            return RedirectToAction("Index", "Home");
        }
    }



LoginView.cs (/Models/ViewModels/LoginView.cs):
    public class LoginView
    {
        [Required(ErrorMessage = "Введите email")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Введите пароль")]
        public string Password { get; set; }

        public bool IsPersistent { get; set; }
    }



Страница для входа Index.cshtml (/Areas/Default/Views/Index.cshtml):

@model LessonProject.Models.ViewModels.LoginView
@{
    ViewBag.Title = "Вход";
    Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
}

<h2>Вход</h2>

@using (Html.BeginForm("Index", "Login", FormMethod.Post, new { @class = "form-horizontal" }))
{
    <fieldset>
        <legend>Вход</legend>
        <div class="control-group">
            <label class="control-label" for="Email">
                Email</label>
            <div class="controls">
                @Html.TextBox("Email", Model.Email, new { @class = "input-xlarge" })
                <p class="help-block">Введите Email</p>
                @Html.ValidationMessage("Email")
            </div>

        </div>
        <div class="control-group">
            <label class="control-label" for="Password">
                Пароль</label>
            <div class="controls">
                @Html.Password("Password", Model.Password, new { @class = "input-xlarge" })
                @Html.ValidationMessage("Password")
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">
                Войти</button>
        </div>
    </fieldset>
}



Запускаем и проверяем:



После авторизации:



Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons
chernikov @chernikov
карма
177,9
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +1
    А я просто делал свой AuthorizeAttribute где проверял залогинен ли юзер и его роли.
    Правда это было давно 2-3 версия, может сейчас что то поменялось. Но работало.
    Примерно как тут описано.
    • +1
      Оно и так тоже будет работать, конечно. Но я не хотел уж совсем всё переписывать.
  • +1
    а что все так сурово, вы по сути выкинули хороший, интегрированный со всем кусок ASP и сделали свой велосипед? Есть хоть одна причина почему не использовать FormsAuthentication + кастомные Membership/Role/Profile провайдеры? DI там и так везде возможны, причем через конструктор а не через DependencyResolver, т.е. с тестированием и моками проблем не возникнет.
    • 0
      плюс с 4.5 ввели ClaimIdentity и ClaimsPrincipal
    • 0
      Там по ссылке есть изыскания на тему того, что стандартный провайдер использует устрашающие хранимые процедуры для своей работы. Но это было в MVC3. А тут используется MVC4 и новый провайдер использует Entity Framework, соответственно вызывает не процедуры, а делает непосредственные запросы. Запросы простые и их немного, как мне кажется врядли велосипедом можно что то наэкономить в плане производительности. Хотя сам по себе опыт по созданию своего провайдера это тоже хорошо.
      Правда тут уже усть новая причина: для доступа к бд используется Linq2SQL и велосипедный мембершип позволит убрать из проекта EF.
      • 0
        Так не важно, как работает дефолтный провайдер. Его можно переопределить, и для этого вовсе не обязательно заниматься вышеописаным изобретением своих Http модулей, фильтров и т.д. а использовать стандартные механизмы.
  • 0
    У стандартного провайдера есть такой недостаток — нельзя удалять пользователей. Либо не использовать перманентные куки, ака галочку «запомнить меня».
    Если с такими куками удаленный из базы пользователь заходит на страницу, то он входит как залогиненный, и контроллер если запросит User.IsAuthenticated, то тоже получает true. То есть мембершип в базу не лезет совсем при этом.

    Мне интересно, решена ли такая проблема в этом примере.
  • 0
    Странности какие то происходят с Http модулями. Попытался сделать что то похожее, никак через web.config не получается зарегистрировать модуль. Брейкпоинтов наставил — управление сразу попадает в контроллер. При этом проект урока, который отсюда скачал — работает. Но у него свой косяк. Первый раз заходит в http модуль, потом попадает в контроллер, как и ожидается, но потом еще раз 10 управление попадает в http модуль, причем из нескольких разных потоков. На всех контроллерах наставил брейков — никуда больше управление не попадает на повторных вызовах…

    Нашел в интернете, как можно подключать модули из кода, сделал у себя — работает. Причем попадает в http модуль один раз на каждый запрос, как и должно. Правда саму авторизацию я еще не реализовал, может еще всплывет что нибудь.
    • 0
      Столкнулся с такой же проблемой. Ответ нашёл здесь: http://stackoverflow.com/questions/15768203/httpmodule-not-called-in-net-4-5

      Вкратце, модуль надо подключать вот так:
      <system.webServer>
      ...
        <modules>
          <add name="AuthHttpModule" type="LessonProject.Global.Auth.AuthHttpModule" />
        </modules>
      </system.webServer>
      

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