Собственная авторизация asp net mvc. Управление контентом в зависимости от роли. Использование HTTP обработчиков сообщений для аутентификации

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

Авторизация - это процесс определения того, имеет ли аутентифицированный пользователь достаточные привилегии на выполнение того или иного действия. Таким действием может быть запрос веб-страницы, доступ к ресурсу, управляемому операционной системой (такому как файл или база данных), либо выполнение специфичных для приложения задач (наподобие размещения заказа в системе управления заказами или назначения проекта в приложении управления проектами вроде Microsoft Project Server).

Некоторые из этих проверок Windows осуществляет автоматически, а другие можно кодировать декларативно, используя файл web.config. Но некоторые проверки придется выполнять непосредственно в коде с использованием объекта IPrincipal.

Авторизация URL

Самый простой способ установки прав доступа предусматривает их установку на индивидуальных веб-страницах, веб-службах и подкаталогах. В идеале платформа веб-приложений должна поддерживать специфичную к ресурсам авторизацию без необходимости изменения кода и перекомпиляции приложения. ASP.NET поддерживает это требование с помощью декларативных правил авторизации, которые можно определять в файле web.config.

Определяемые вами правила действуют через специальный модуль HTTP по имени UrlAuthorizationModule . Модуль просматривает эти правила и проверяет каждый запрос, гарантируя, что пользователю не будут доступны ресурсы, доступ к которым ограничен специальным образом.

Авторизация такого типа называется авторизацией URL, потому что принимает во внимание только две детали - контекст безопасности пользователя и URL ресурса, к которому пользователь пытается обратиться. Если доступ к странице запрещен и применяется аутентификация с помощью форм, то пользователь будет перенаправлен на страницу регистрации.

Правила авторизации

Другими словами, существуют два типа правил: разрешить (allow) и запретить (deny). Тех и других можно добавлять столько угодно. Каждое правило идентифицирует одного или более пользователей либо ролей (групп пользователей). Вдобавок можно использовать атрибут verbs, чтобы создавать правила, применимые только к специфичным типам HTTP-запросов (GET, POST, HEAD ИЛИ DEBUG).

В предыдущих статьях уже приводился простейший пример. Чтобы запретить доступ всем анонимным пользователям, можно определить правило вроде следующего:

В данном случае вопросительный знак (?) - это шаблонный символ, представляющий всех пользователей с неизвестной идентичностью. Это правило почти всегда используется в сценариях аутентификации. Это связано с невозможностью запрета доступа другим, известным пользователям, если только сначала не заставить всех аутентифицировать себя.

Можно использовать дополнительный шаблонный символ - звездочку (*), которая представляет всех пользователей. Например, следующий раздел открывает доступ аутентифицированным и анонимным пользователям:

Такое правило требуется редко, потому что оно уже присутствует в файле machine.config. После того как среда ASP.NET применит все правила из файла web.config, она применяет правила из machine.config. В результате любому пользователю, кому явно закрыт доступ, автоматически его получает.

Теперь посмотрим, что произойдет, если в раздел добавить более одного правила:

При оценке правил среда ASP.NET сканирует список сверху вниз. Как только она обнаружит применимое правило, поиск прекращается. Поэтому в предыдущем случае она определит, что правило применимо к текущему запросу и не станет принимать во внимание вторую строку. В результате это правило разрешит доступ всем пользователям. Однако если две строки поменять местами, то доступ будет закрыт анонимным пользователям (согласно правилу в первой строке) и разрешен всем остальным (согласно правилу во второй строке).

Когда правила авторизации добавляются в файл web.config корневого каталога веб-приложения, они автоматически применяются ко всем веб-ресурсам, которые являются частью приложения. Если вход анонимным пользователям запрещен, ASP.NET проверит метод аутентификации. Если выбрана аутентификация с помощью форм, ASP.NET перенаправит пользователя на страницу регистрации.

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

Конфигурирование доступа для определенных пользователей

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

Например, следующее правило авторизации специально ограничивает доступ для трех пользователей. Эти пользователи не смогут обратиться к страницам, которые находятся в каталоге с файлом web.config, содержащим эти вхождения. Все остальные аутентифицированные пользователи обращаться к этим страницам смогут:

Можно также определить список имен, разделенный запятыми, и запретить доступ сразу нескольким пользователям. Ниже показана версия, эквивалентная предыдущему примеру, в которой используются только два правила аутентификации:

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

Создавая безопасные приложения, часто лучше явно разрешить доступ определенным пользователям и группам, а затем запретить всем остальным (вместо запрета доступа определенным пользователям, как в предыдущих примерах). Ниже приведен пример правил авторизации, которые явно открывают доступ двум пользователям. Запросы всех прочих пользователей не будут удовлетворены, даже несмотря на то, что они аутентифицированы:

Следует обратить внимание на одну деталь. Формат имен пользователей в этих примерах предполагает аутентификацию с помощью форм. При такой аутентификации имя пользователя назначается при вызове метода RedirectFromLoginPage(). В этой точке UrlAuthorizationModule также использует это имя и проверит его по списку правил авторизации.

Контроль доступа к определенным каталогам

Общепринятое проектное решение для приложения предусматривает размещение файлов, которые требуют аутентификации, в отдельном каталоге. Благодаря конфигурационным файлам ASP.NET, этот подход реализуется просто. Оставьте элемент

Вспомните, что когда вы добавляете файл web.config в подкаталог, он не должен содержать никаких других специфичных для приложения настроек. Фактически он должен содержать только информацию авторизации, как показано ниже:

Настройки дескриптора в файле web.config подкаталога приложения изменять нельзя. Вместо этого все каталоги в приложении должны использовать одну и ту же систему аутентификации. Однако каждый каталог может иметь собственный набор правил авторизации.

При использовании правил авторизации в подкаталоге среда ASP.NET все равно читает правила авторизации из родительского каталога. Отличие в том, что правила из подкаталога применяются первыми. Это важно, потому что ASP.NET останавливается, как только находит соответствие правила авторизации. Рассмотрим пример, в котором корневой виртуальный каталог содержит одно правило:

а подкаталог - другое правило:

В этом случае пользователь dan сможет получить доступ к любому ресурсу корневого каталога, но не получит никакого доступа к ресурсам из подкаталога. Если же вы поменяете местами эти два правила, то dan получит доступ только к ресурсам подкаталога, но не получит доступа к ресурсам корневого каталога.

Более интересно то, что ASP.NET допускает неограниченную иерархию подкаталогов и правил авторизации. Например, вполне возможно иметь виртуальный каталог с правилами авторизации, подкаталог, определяющий дополнительные права, и еще один подкаталог внутри этого подкаталога, в котором определен свой набор правил. Проще всего понять процесс авторизации в этом случае - представить все правила в виде единого списка, начинающегося с каталога, в котором находится запрошенная страница. Если все эти правила обработаны и соответствие не найдено, ASP.NET начинает читать правила авторизации из родительского каталога, затем из каталога, родительского по отношению к этому, и т.д. - до тех пор, пока соответствие не будет найдено. Если никаких подходящих правил не найдено, ASP.NET в конечном итоге применяет правило из файла machine.config.

Контроль доступа к определенным файлам

Обычно установка прав доступа к файлам на уровне каталога - самый ясный и легкий подход. Однако существует возможность ограничить доступ к определенным файлам за счет добавления дескрипторов в файл web.config.

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

В этом примере открывается доступ ко всем файлам приложения за исключением SecuredPage.aspx и AnotherSecuredPage.aspx, которые имеют правило авторизации, запрещающее анонимный доступ к ним.

Конфигурирование доступа для определенных ролей

Для облегчения понимания и сопровождения системы безопасности веб-сайта пользователей часто объединяют в категории, называемые ролями. Если нужно управлять приложением масштаба предприятия, которое поддерживает тысячи пользователей, понять значение ролей несложно. Если бы пришлось определять права доступа для каждого индивидуального пользователя, это было бы утомительно, изменять это было бы трудно, а не совершить при этом ошибок - практически невозможно.

Сама по себе аутентификация с помощью форм роли не поддерживает, но наряду с Membership API в ASP.NET доступен интерфейс API Roles , который представляет собой готовую к использованию реализацию поддержки и управления ролями для приложений. Кроме того, если вы не хотите использовать эту инфраструктуру, достаточно легко создать собственную, которая разделит пользователей на соответствующие группы на основе их удостоверений.

После определения для ролей можно создавать правила авторизации. Фактически эти правила выглядят точно так же, как показанные ранее правила, специфичные для пользователей. Например, следующие правила авторизации запрещают доступ анонимным пользователям, разрешая его двум конкретным пользователям (dan и alex) и двум группам (Manager и Supervisor). Доступ всем прочим пользователям запрещен:

Например, рассмотрим, что произойдет, если вы разрешите доступ группе, которая содержит определенного пользователя, а затем явно закроете доступ этому пользователю. Или наоборот - разрешив доступ пользователю по имени, вы закроете доступ группе, в которую он входит. В этих сценариях можно ожидать, что более тонко детализированные правила (касающиеся пользователя) получат приоритет по отношению к более общим правилам (касающимся групп). Или же вы можете ожидать, что более ограничивающие правила всегда будет иметь более высокий приоритет, как это принято в Windows. Однако ни один из этих подходов не используется ASP.NET. Вместо этого ASP.NET просто применяет первое подходящее правило. В результате решающее значение имеет порядок определения правил .

Рассмотрим пример:

Ниже описано, как ASP.NET разбирает эти правила:

    В этом примере доступ пользователю alex будет разрешен независимо от групп, к которым он принадлежит.

    Всем пользователям из роли Guest доступ будет закрыт. Если alex входит в Guest, доступ ему остается открытым, потому что специфичное для пользователя правило найдено первым.

    Далее всем пользователям из группы Manager доступ открыт. Единственным исключением будут те, что входят и в группу Manager и в Guest. Правило Guest определено раньше в списке, поэтому данным пользователям вход уже закрыт.

    Затем доступ закрывается пользователю dan. Но если dan входит в группу Manager, доступ для него остается открытым.

    Любым пользователям, относящимся к группе Supervisor, кроме тех, доступ для которых был открыт или закрыт предыдущими правилами, доступ открывается.

    И, наконец, для всех прочих пользователей доступ закрыт.

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

Файловая авторизация

Авторизация на основе URL - один из краеугольных камней авторизации ASP.NET. Однако в ASP.NET также используется другой тип авторизации, который часто пропускается или игнорируется многими разработчиками. Это авторизация на основе файлов, реализуемая модулем FileAuthorizationModule . Авторизация на основе файлов работает, только в случае применения Windows-аутентификации. Если же используется специальная аутентификация или аутентификация с помощью форм, файловая авторизация не применяется.

Чтобы понять суть файловой авторизации, необходимо разобраться, как операционная система Windows обеспечивает безопасность файловой системы. В случае файловой системы NTFS можно установить списки ACL (access control list - список контроля доступа) , указывающие идентичность пользователей и ролей, которым открыт или запрещен доступ к индивидуальным файлам. Модуль FileAuthorizationModule просто проверяет права доступа к запрошенному файлу, определенные Windows.

Например, если запрашивается веб-страница, FileAuthorizationModule проверяет, имеет ли текущий аутентифицированный IIS пользователь права доступа к соответствующему файлу.aspx. Если не имеет, то код страницы не выполняется и пользователь получает сообщение о запрете доступа.

Новые пользователи ASP.NET часто удивляются, почему файловая авторизация должна быть реализована отдельным модулем, и почему бы ни положиться в этом на операционную систему?

Чтобы понять необходимость в модуле FileAuthorizationModule, необходимо вспомнить, как ASP.NET выполняет код. Если не включено заимствование прав, ASP.NET выполняется от имени фиксированного пользовательской учетной записи, такой как ASPNET. Операционная система Windows будет проверять, имеет ли учетная запись ASPNET права, необходимые для доступа к файлу.aspx, но она не выполнит ту же проверку для пользователя, аутентифицированного IIS. Модуль FileAuthorizationModule заполняет этот пробел. Он осуществляет проверку авторизации с учетом контекста безопасности текущего пользователя. В результате системный администратор может устанавливать права доступа к файлам или папкам и контролировать доступ к частям приложения ASPNET. Обычно проще и удобнее использовать правила авторизации в файле web.config. Однако если необходимо воспользоваться преимуществами существующих привилегий Windows в локальной или корпоративной сети, то это можно сделать.

  • Tutorial

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

Небольшое отступление: На самом деле в asp.net mvc все учебники рекомендуют пользоваться уже придуманной системой авторизации, которая называется AspNetMembershipProvider, она была описана в статье (сейчас доступ уже закрыт), но обьяснено это с точки зрения «нажимай и не понимай, что там внутри». При первом знакомстве с asp.net mvc меня это смутило. Далее, в этой статье - сказано, что пользоваться этим провайдером – нельзя. И я согласен с этим. Здесь же, мы достаточно глубоко изучаем всякие хитрые 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 { ///

/// Конекст (тут мы получаем доступ к запросу и кукисам) /// 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; } 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; 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:
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 { ///

/// Текщий пользователь /// public User User { get; set; } … public IAuthentication Auth { get; set; } public User CurrentUser { get { return ((IUserProvider)Auth.CurrentUser.Identity).User; } }
А теперь попробуем инициализировать это всё.
Вначале добавим наш IAuthentication + CustomAuthentication в регистрацию к Ninject (/App_Start/NinjectWebCommon.cs):

Kernel.Bind().To().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(); auth.HttpContext = context; context.User = auth.CurrentUser; } public void Dispose() { } }

Вся соль в строках: auth.HttpContext = context и context.User = auth.CurrentUser . Как только наш модуль авторизации узнает о контексте и содержащихся в нем кукисах, ту же моментально получает доступ к имени, по нему он в репозитории получает данныепользователя и возвращает в BaseController. Но не сразу всё, а по требованию.
Подключаем модуль в Web.config:

План таков:

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

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

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

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

@model LessonProject.Model.User @if (Model != null) {

  • @Model.Email
  • @Html.ActionLink("Выход", "Logout", "Login")
  • } else {
  • @Html.ActionLink("Вход", "Index", "Login")
  • @Html.ActionLink("Регистрация", "Register", "User")
  • }

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

    Public class LoginController: DefaultController { public ActionResult Index() { return View(new LoginView()); } 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 { public string Email { get; set; } 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"; }

    Вход

    @using (Html.BeginForm("Index", "Login", FormMethod.Post, new { @class = "form-horizontal" })) {
    Вход
    @Html.TextBox("Email", Model.Email, new { @class = "input-xlarge" })

    Введите Email

    @Html.ValidationMessage("Email")
    @Html.Password("Password", Model.Password, new { @class = "input-xlarge" }) @Html.ValidationMessage("Password")
    }

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

    Все исходники находятся по адресу

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

    Релиз ASP.NET MVC 5 ознаменовался выходом новой системой авторизации и аутентификации в.NET приложениях под названием ASP.NET Identity. Эта система пришла на смену провайдерам Simple Membership, которые были введены в ASP.NET MVC 4.

    Нажав на кнопку Change Authentication , мы можем изменить тип аутентификации, выбрав одно из следующих:

      No Authentication : ASP.NET Identity и встроенная система аутентификации отсутствует

      Individual User Accounts : проект по умолчанию включает систему ASP.NET Identity, которая позволяет авторизовать как пользователей внутри приложения, так и с помощью внешних сервисов, как google, твиттер и т.д.

      Organizational Accounts : подходит для сайтов и веб-приложений отдельных компаний и организаций

      Windows Authentication : система аутентификации для сетей intranet с помощью учетных записей Windows

    Оставим значение по умолчанию, то есть Individual User Accounts и создадим проект.

    Созданный проект уже по умолчанию имеет всю необходимую для авторизации инфраструктуру: модели, контроллеры, представления. Если мы заглянем в узел References (Библиотеки), то увидим там ряд ключевых библиотек, которые и содержит необходимые для авторизации и аутентификации классы:

    Это ряд библиотек OWIN, которые добавляют функциональность OWIN в проект, а также три библиотеки собственно ASP.NET Identity:

      Microsoft.AspNet.Identity.EntityFramework : содержит классы Entity Framework, применяющие ASP.NET Identity и осуществляющие связь с SQL Serveroм

      Microsoft.AspNet.Identity.Core : содержит ряд ключевых интерфейсов ASP.NET Identity. Реализация этих интерфейсов позволит выйти за рамки MS SQL Server и использовать в качестве хранилища учетных записей другие СУБД, в том числе системы NoSQL

      Microsoft.AspNet.Identity.OWIN : привносит в приложение ASP.NET MVC аутентификацию OWIN с помощью ASP.NET Identity

    Поскольку вся инфраструктура уже имеется в проекте, запустим проект, на отобразившейся в браузере странице найдем ссылку Register и нажмем на нее. На открывшейся странице регистрации введем какие-нибудь данные:

    После регистрации логин будет отображаться в правом верхнем углу веб-страницы веб-приложения. Осуществив регистрацию, мы можем разлогиниться, нажав на LogOff, и снова войти в систему. Таким образом, мы можем уже начать пользоваться встроенной системой аутентификации в приложении ASP.NET MVC 5. Теперь же рассомотрим ее основные моменты.

    Во-первых, где это все хранится? Куда попадают данные зарегистрированных пользователей?

    В данном случае используется подход Code First. В файле web.config уже имеется строка подключения по умолчанию, которая задает каталог базы данных. Если мы раскроем папку App_Data, то сможем увидеть созданную базу данных:

    Если вдруг в папке база данных не видна, нажмем вверху окна Solution Explorer на кнопку Show All Files (Показать все файлы).

    Мы можем открыть эту базу данных в окне Server Explorer и увидеть ее содержимое:

    По умолчанию при регистрации первого пользователя создается следующий набор таблиц:

      MigrationHistory : используется EntityFramework для миграций БД

      AspNetRoles : содержит определения ролей

      AspNetUserClaims : таблица, хранящая набор клеймов (claim). Claim представляет иную модель авторизации по сравнению с ролями. Грубо говоря, claim содержит некоторую информацию о пользователе, например, адрес электронной почты, логин, возраст и т.д. И эта информация позволяет идентифицировать пользователя и наделить его соответствующими правами доступа.

      AspNetUserLogins : таблица логинов пользователя

      AspNetUserRoles : таблица, устанавливающая для пользователей определенные роли

      AspNetUsers : собственно таблица пользователей. Если мы ее откроем, то увидим данные зарегистрированного пользователя

    Ключевыми объектами в AspNet Identity являются пользователи и роли . Вся функциональность по созданию, удалению пользователей, взаимодействию с хранилищем пользователей хранится в классе UserManager . Для работы с ролями и их управлением в AspNet Identity определен класс RoleManager . Классы UserManager и RoleManager находятся в библиотеке Microsoft.AspNet.Identity.Core.

    Каждый пользователь для UserManager представляет объект интерфейса IUser. А все операции по управлению пользователями производятся через хранилище, представленное объектом IUserStore.

    Каждая роль представляет реализацию интерфейса IRole, а управление ролями классом RoleManager происходит через хранилище IRoleStore.

    Непосредственную реализацию интерфейсов IUser, IRole, IUserStore и IRoleStore предоставляет пространство имен Microsoft.AspNet.Identity.EntityFramework:

    Класс IdentityUser является реализацией интерфейса IUser. А класс хранилища пользователей - UserStore реализует интерфейс IUserStore.

    Подобным образом класс IdentityRole реализует интерфейс IRole, а класс хранилища ролей - RoleStore реализует интерфейс IRoleStore.

    А для взаимодействия с базой данных в пространстве имен Microsoft.AspNet.Identity.EntityFramework определен класс контекста IdentityDbContext

    В приложении ASP.NET MVC мы не будем работать напрямую с классами IdentityUser и IdentityDbContext. По умолчанию в проект в папку Models добавляется файл IdentityModels.cs , который содержит определения классов пользователей и контекста данных:

    Public class ApplicationUser: IdentityUser { public async Task GenerateUserIdentityAsync (UserManager manager) { var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); return userIdentity; } } public class ApplicationDbContext: IdentityDbContext { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }

    В приложении мы не работаем напрямую с классами IdentityUser и IdentityDbContext, а имеем дело с классами-наследниками.

    Класс ApplicationUser наследует от IdentityUser все свойства. И кроме того добавляет метод GenerateUserIdentityAsync() , в котором с помощью вызова UserManager.CreateIdentityAsync создается объект ClaimsIdentity . Данный объект содержит информацию о данном пользователе.

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

    Я не буду подробно расписывать весь функционал AspNet Identity, который по умолчанию добавляется в проект, обозначу вкратце лишь основные возможности.

    Во-первых, чтобы задействовать AspNet Identity, в проект в папку App_Start добавляются два файла. Файл Startup.Auth.cs содержит класс запуска приложения OWIN. Поскольку AspNet Identity использует инфраструктуру OWIN, то данный класс является одним из ключевых и необходимых для работы.

    Файл IdentityConfig.cs содержит ряд дополнительных вспомогательных классов: сервисы для двухфакторной валидации с помощью email и телефона EmailService и SmsService , класс менеджера пользователей ApplicationUserManager , добавляющий к UserManager ряд дополнительных функций, и класс ApplicationSignInManager , используемый для входа и выхода с сайта.

    Базовая функциональность системы аутентификации и управления учетными записями расположена в двух контроллерах: AccountController и ManageController

    В AccountController определены методы для логина, регистрации, верификации кода, отправленного по email или по смс, сброс пароля, напоминание пароля, вход на сайт с помощью внешних сервисов. Контроллер ManageController используется для управления учетной записью и предполагает возможности по смене пароля и управлению телефонными номерами в системе. Для обоих контроллеров уже по умолчанию генерируются все необходимые представления и специальные модели представлений.

    Несмотря на то, что по умолчанию нам уже предоставляется готовый функционал, однако в ряде случаев, например, для отправки смс или электронной почты необходима дополнительная настройка. Теперь рассмотрим основные моменты системы AspNet Identity.

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

    Аутентификация подтверждает, что пользователь предоставил корректные данные для доступа в систему. Когда пользователь входит в систему (как правило, с помощью имени пользователя (логина) и пароля, но возможны и некоторые другие маркеры, такие как ключ SSH или зашифрованный ключ), он аутентифицирован.

    Авторизация происходит после аутентификации и подразумевает принятие решения о том, имеет ли данный пользователь разрешение сделать что-либо в системе, например, просмотреть страницу или отредактировать запись. Если пользователь получает доступ к ресурсу, который не доступен для других, то он был специально авторизирован для этого.

    Ограничение доступа с AuthorizeAttribute

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

    По умолчанию новые проекты ASP.NET MVC, созданные на основе шаблона проектов Internet Application , для включения аутентификации используют схему forms-аутентификации, которая определена в разделе system.web/authentication в файле web.config:

    Когда включена forms-аутентификация и пользователь пытается получить доступ к закрытому ресурсу, он будет перенаправлен на LoginUrl для того, чтобы ввести имя пользователя и пароль.

    Windows-аутентификация

    В качестве альтернативы forms-аутентификации ASP.NET также поддерживает Windows-аутентификацию, которую можно включить, изменив на в web.config .

    Windows-аутентификация попытается выполнить проверку пользователя с помощью учетной записи Windows с данными пользователя, и это лучше всего подходит для интранет-приложений, где пользователь входит в систему на том же домене, где находится приложение. На самом деле, эта схема аутентификации используется по умолчанию для шаблона проекта Intranet Application в ASP.NET MVC.

    Когда аутентификация включена, мы можем применить AuthorizeAttribute к действиям контроллера (или даже целым контроллерам), чтобы ограничить доступ к ним. Если пользователь не имеет доступа к действию, AuthorizeAttribute передаст в браузер код статуса HTTP 401 Unauthorized , который указывает, что запрос был отклонен. Приложения, в которых используется forms-аутентификация, будут перенаправлять браузер на страницу входа, и пользователи смогут продолжить, только если выполнят вход.

    Простейшее применение AuthorizeAttribute требует только аутентификации текущего пользователя:

    Public ActionResult About() { return View(); }

    Неаутентифицированным пользователям будет запрещен доступ к этому действию, но любому аутентифицированныму пользователю доступ будет разрешен.

    Чтобы ограничить действие далее, мы можем указать пользователей или роли, которые требует AuthorizeAttribute . Эти пользователи или роли передаются в атрибут в виде списка строк, разделенных запятыми и содержащих имена пользователей или ролей с правами доступа:

    Public ActionResult Admins() { return View(); }

    В этом случае только пользователь с именем "admin" будет иметь доступ к данному действию.

    Жесткое кодирование имени пользователя, как показано здесь, может быть слишком явным - пользователи приходят и уходят, и обязанности определенного пользователя могут изменяться по ходу использования приложения. Вместо требования конкретного имени пользователя обычно имеет смысл требовать роль:

    Public ActionResult Developers() { return View(); }

    Доступ к действию Developers будет разрешен только для пользователей в роли администратора или разработчика - для всех других пользователей (аутентифицированных или нет) будет выдан код ответа 401 и, с помощью forms-аутентификации ASP.NET, они будут перенаправлены на страницу входа.

    Аутентификация на основе ролей

    Аутентификация на основе ролей может потребовать некоторых дополнительных настроек, в зависимости от схемы аутентификации, которую вы используете.

    Если вы используете Windows-аутентификацию, роли будут автоматически найдены в групповом членстве Active Directory. Однако если вы используете forms-аутентификацию, вам нужно будет использовать провайдер членства (который может быть настроен в web.config), чтобы определить, где нужно сохранять и находить информацию о пользователях (например, роли).

    Шаблон проектов для ASP.NET MVC Intranet Application по умолчанию будет использовать базу данных SQL Express для хранения ролей.

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

    AuthorizeAttribute - как он работает

    Внутренне AuthorizeAttribute реализован в виде IAuthorizationFilter , который выполняет несколько проверок, чтобы решить, имеет ли пользователь право доступа к текущему действию контроллера. Процесс принятия решений данного атрибута показан на рисунке 8-1.

    Рисунок 8-1: AuthorizeAttribute проверяет, аутентифицирован ли пользователь, есть ли имя пользователя в белом списке и какова его роль, прежде чем принять решение, имеет ли пользователь права для просмотра требуемого действия

    Поскольку AuthorizeAttribute реализует интерфейс IAuthorizationFilter , он должен содержать метод под названием OnAuthorization , который получает ссылку на AuthorizationContext , представляющий текущий запрос.

    Когда фреймворк вызывает этот метод, атрибут получает ссылку на текущий IPrincipal , который соответствует пользователю, выполняющему текущий запрос. Если пользователь еще не прошел аутентификацию, он отменяет запрос, установив значение свойства Result класса AuthorizationContext на HttpUnauthorizedResult . Это отменяет вызов действия контроллера и отправляет в браузер код HTTP 401 , который, в свою очередь, вызывает соответствующий запрос входа.

    Если указаны пользователи или роли, AuthorizeAttribute проверяет, находится ли текущее имя пользователя в списке разрешенных имен, или приписана ли пользователю роль с правом доступа. Если же Users или Roles не указаны, пользователь получает права доступа.

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

    AuthorizeAttribute можно использовать несколькими способами:

    • Если AuthorizeAttribute применяется к контроллеру, он применяется к каждому действию этого контроллера
    • Если несколько AuthorizeAttribute применяются к действию, пользователь должен пройти все проверки и быть авторизированным каждым из них

    В ASP.NET MVC существует несколько других реализаций IAuthorizationFilter , и все они используются для защиты от нежелательных запросов.

    В главе 16 фильтры будут описаны подробно, но давайте рассмотрим пять фильтров, которые связаны непосредственно с безопасностью:

    • AuthorizeAttribute: Об этом вы уже знаете
    • ChildActionOnlyAttribute: Гарантирует, что метод действия может быть вызван только из другого действия (обычно из представления с помощью Html.Action), но не может быть вызван напрямую
    • RequireHttpsAttribute: Гарантирует, что действие может быть доступно только через безопасное соединение
    • ValidateAntiForgeryTokenAttribute: Гарантирует, что был указан действительный маркер для борьбы с фальсификацией (вы узнаете об этом больше в следующем разделе)
    • ValidateInputAttribute: Указывает, должен ли ASP.NET проводить валидацию пользовательского ввода для обнаружения потенциально опасного содержимого

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

    Скажем, что у вас есть.net web api с действием GetResource (int resourceId). Это действие (с указанным идентификатором) должно быть разрешено только для пользователя, связанного с этим идентификатором (например, ресурс может быть блогером, написанным пользователем).

    Это можно решить разными способами, но ниже приведен пример.

    Public Resource GetResource(int id) { string name = Thread.CurrentPrincipal.Identity.Name; var user = userRepository.SingleOrDefault(x => x.UserName == name); var resource = resourceRepository.Find(id); if (resource.UserId != user.UserId) { throw new HttpResponseException(HttpStatusCode.Unauthorized); } return resource; }

    где пользователь был аутентифицирован каким-то механиком.

    Теперь скажем, что я также хочу, чтобы пользователь, например, типа администратора, имел право потреблять конечную точку (с тем же идентификатором). Этот пользователь не имеет прямого отношения к ресурсу, но имеет авторизацию из-за его типа (или роли). Это можно решить, просто проверив, имеет ли пользователь тип администратора и возвращает ресурс.

    Есть ли способ централизовать это таким образом, чтобы мне не приходилось писать код авторизации в каждом действии?

    Edit Основываясь на ответах, я думаю, что должен уточнить свой вопрос.

    То, что я действительно делаю, - это механизм, который позволяет иметь авторизацию на основе ресурсов, но в то же время позволяет некоторым пользователям также использовать одну и ту же конечную точку и тот же ресурс. Ниже приведенное действие разрешит это для этой конкретной конечной точки и для этой конкретной роли (Admin).

    Public Resource GetResource(int id) { string name = Thread.CurrentPrincipal.Identity.Name; var user = userRepository.SingleOrDefault(x => x.UserName == name); var resource = resourceRepository.Find(id); if (!user.Roles.Any(x => x.RoleName == "Admin" || resource.UserId != user.UserId) { throw new HttpResponseException(HttpStatusCode.Unauthorized); } return resource; }

    То, что я получаю, - это общий способ решить эту проблему, так что мне не нужно писать две разные конечные точки с той же целью или писать код, специфичный для ресурсов на каждой конечной точке.

    4 ответов

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

    Public Resource GetResource(int id) { var resource = resourceRepository.Find(id); if (resource.UserId != User.Identity.GetUserId()) { throw new HttpResponseException(HttpStatusCode.Unauthorized); } return resource; }

    Если вы хотите еще больше упростить код, вы можете написать UserRepository, который знает данные пользователя и репозиторий ресурсов для централизации кода. Код будет выглядеть так:

    Public Resource GetResource(int id) { return User.Identity.GetUserRepository().FindResource(id); }

    Для авторизации на основе роли AuthorizeAttribute будет лучшим местом для ее обработки, и вам лучше использовать отдельное действие или контроллер для этого.

    Public Resource GetResourceByAdmin(int id) { return resourceRepository.Find(id); }

    [Изменить] Если OP хочет использовать одно действие для работы с разными типами пользователей, я лично предпочитаю использовать репозиторий пользователя factory. Код действия:

    Public Resource GetResource(int id) { return User.GetUserRepository().FindResource(id); }

    Метод расширения будет:

    Public static IUserRepository GetUserRepository(this IPrincipal principal) { var resourceRepository = new ResourceRepository(); bool isAdmin = principal.IsInRole("Admin"); if (isAdmin) { return new AdminRespository(resourceRepository); } else { return new UserRepository(principal.Identity, resourceRepository); } }

    Причина, по которой я не хочу использовать AuthorizeAttribute для проверки подлинности каждого ресурса, заключается в том, что у разных ресурсов может быть другой код для проверки права собственности, трудно централизовать код в одном атрибуте, и для этого требуются дополнительные операции с БД, что на самом деле не является необходимо. Еще одна проблема заключается в том, что AuthroizeAttribute происходит до привязки параметров, поэтому вам нужно убедиться, что параметр действия поступает из данных маршрута. В противном случае, например, из тела сообщения вы не сможете получить значение параметра.

    Вам необходимо выполнить экстернализацию своей авторизации. Вы хотите переместить всю логику авторизации на отдельный уровень или службу.

    Существует несколько рамок - на разных языках - которые позволяют вам это делать. В мире.NET, как предложено в других ответах, у вас есть авторизация на основе требований. У Microsoft есть отличная статья о .

    • стандартная архитектура с понятием точки принятия решения о политике (PDP - это ваша служба авторизации), которая может служить да/нет решений
    • стандартный язык для выражения логики авторизации с использованием любого количества параметров/атрибутов, включая атрибуты пользователя и информацию о ресурсе.
    • схема запроса/ответа для отправки ваших вопросов авторизации в PDP.

    Если мы перейдем к вашему примеру, у вас будет что-то по строкам:

    Public Resource GetResource(int id) { var resource = resourceRepository.Find(id); if (isAuthorized(User.Identity,resource)) { throw new HttpResponseException(HttpStatusCode.Unauthorized); } return resource; } public bool isAuthorized(User u, Resource r){ // Create XACML request here // Call out to PDP // return boolean decision }

    Ваш PDP будет содержать следующие правила:

    • пользователь может выполнить действие == представление на ресурсе тогда и только тогда, когда resource.owner == user.id
    • пользователь с ролью == administrator может выполнить действие == на ресурсе.

    Преимущество XACML заключается в том, что вы можете вырабатывать свои правила/логику авторизации независимо от вашего кода. Это означает, что вам не нужно касаться вашего кода приложения всякий раз, когда изменяется логика. XACML также может обслуживать больше параметров/атрибутов - например, идентификатор устройства, IP-адрес, время суток... Наконец, XACML не относится к.NET. Он работает для разных рамок.

    Я бы посмотрел на реализацию пользовательского System.Web.Http.AuthorizeAttribute , который вы могли бы применить к действиям, которые нуждаются в этом конкретном правиле авторизации. В пользовательской авторизации вы можете разрешить доступ, если пользователь является членом группы "Админы", или если он является автором ресурса.

    ИЗМЕНИТЬ:

    Основываясь на редактировании OP, позвольте мне расширить то, что я говорю. Если вы переопределите AuthorizeAttribute, вы можете добавить логику, например:

    Public class AuthorizeAdminsAndAuthors: System.Web.Http.AuthorizeAttribute { protected override bool IsAuthorized(HttpActionContext actionContext) { return currentUser.IsInRole("Admins") || IsCurrentUserAuthorOfPost(actionContext); } private bool IsCurrentUserAuthorOfPost(HttpActionContext actionContext) { // Get id for resource from actionContext // look up if user is author of this post return true; }

    Это псевдокод, но должен передать идею. Если у вас есть один атрибут AuthorizeAttribute, который определяет авторизацию на основе ваших требований: текущий запрос - либо от автора сообщения, либо от администратора, то вы можете применить атрибут AuthorizeAdminsAndAuthors к любому ресурсу, где требуется этот уровень авторизации. Таким образом, ваш ресурс будет выглядеть так:

    Public Resource GetResource(int id) { var resource = resourceRepository.Find(id); return resource; }



    
    Top