Последнее обновление: 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 Server

    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.

Authorization determines whether an identity should be granted access to a specific resource. In ASP.NET, there are two ways to authorize access to a given resource:

    File authorization File authorization is performed by the . It checks the access control list (ACL) of the .aspx or .asmx handler file to determine whether a user should have access to the file. ACL permissions are verified for the user"s Windows identity (if Windows authentication is enabled) or for the Windows identity of the ASP.NET process. For more information, see ASP.NET Impersonation .

    URL authorization URL authorization is performed by the , which maps users and roles to URLs in ASP.NET applications. This module can be used to selectively allow or deny access to arbitrary parts of an application (typically directories) for specific users or roles.

Using URL Authorization

With URL authorization, you explicitly allow or deny access to a particular directory by user name or role. To do so, you create an authorization section in the configuration file for that directory. To enable URL authorization, you specify a list of users or roles in the or elements of the section of a configuration file. The permissions established for a directory also apply to its subdirectories, unless configuration files in a subdirectory override them.

The following shows the syntax for the authorization section:

< users roles verbs />

The allow or deny element is required. You must specify either the users or the roles attribute. Both can be included, but both are not required. The verbs attribute is optional.

The allow and deny elements grant and revoke access, respectively. Each element supports the attributes shown in the following table:

Identifies the targeted identities (user accounts) for this element.

Anonymous users are identified using a question mark (?). You can specify all authenticated users using an asterisk (*).

Defines the HTTP verbs to which the action applies, such as GET, HEAD, and POST. The default is "*", which specifies all verbs.

The following example grants access to the Kim identity and members of the Admins role, and denies access to the John identity (unless the John identity is included in the Admins role) and to all anonymous users:

The following authorization section shows how to allow access to the John identity and deny access to all other users:

You can specify multiple entities for both the users and roles attributes by using a comma-separated list, as shown in the following example:

Note that if you specify a domain account name, the name must include both the domain and user name (contoso\Jane).

The following example allows all users to perform an HTTP GET for a resource, but allows only the Kim identity to perform a POST operation:

Rules are applied as follows:

    Rules contained in application-level configuration files take precedence over inherited rules. The system determines which rule takes precedence by constructing a merged list of all rules for a URL, with the most recent rules (those nearest in the hierarchy) at the head of the list.

    Given a set of merged rules for an application, ASP.NET starts at the head of the list and checks rules until the first match is found. The default configuration for ASP.NET contains an element, which authorizes all users. (By default, this rule is applied last.) If no other authorization rules match, the request is allowed. If a match is found and the match is a deny element, the request is returned with the 401 HTTP status code. If an allow element matches, the module allows the request to be processed further.

  • 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")
    }

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

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

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

    Аутентификация подтверждает, что пользователь предоставил корректные данные для доступа в систему. Когда пользователь входит в систему (как правило, с помощью имени пользователя (логина) и пароля, но возможны и некоторые другие маркеры, такие как ключ 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 может помочь в управлении аутентификацией и авторизацией, так что давайте обратим наше внимание на другие, более коварные направления атак. Хотя аутентификация и авторизация и закрывают случайным посетителям доступ к защищенным областям, вы все еще должны защитить вашу программу от хакеров и воров, которые пытаются использовать уязвимости веб-приложений. Далее в этой главе мы рассмотрим несколько распространенных атак и уязвимостей и способы защиты от них.

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

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

    Корпоративные сайты, расположенные в защищённых внутренних сетях предприятий, могут содержать страницы с отчётами, предназначенными только для руководителей или сотрудников определённых отделов. Рядовые сотрудники не должны иметь доступа к этим ресурсам.

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

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

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

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

    Что такое авторизация?

    Как мы уже говорили, предоставление пользователям уникальных учётных записей позволяет идентифицировать их. Механизм «узнавания » сайтом зарегистрированных посетителей называется аутентификацией.

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

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

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

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

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

    Управление доступом на основе групп и ролей

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

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

    Новый пользователь на сайте включается в какую-либо группу. Это может делаться автоматически либо вручную, администратором сайта. Таким образом, пользователь получает заранее определённый набор прав и ограничений.

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

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

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

    Хорошей практикой является разделение пользователей на группы по виду деятельности на сайте. Например, наш блог может иметь следующие группы пользователей: «Авторы », «Редакторы », «Модераторы » и т.д.

    Альтернативный подход будет состоять в том, чтобы выделить группы «Создание статей », «Правка статей », «Удаление комментариев » и т. д. Такой подход будет обладать значительной гибкостью, но при этом придётся поддерживать на сайте большее количество различных групп.

    Защита страниц средствами ASP.NET

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

    При защите сайта на ASP.NET используются три направления разделения доступа:

    • Система роутинга адресов;
    • Веб-формы (файлы и папки );
    • Структуры MVC .

    Защита сайта на веб-формах

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

    XML -элемент location определяет путь к защищаемому ресурсу: папке, странице или элементу роутинга. В данном примере он задаёт страницу adminhome.aspx . Можно защитить содержимое папки целиком, указав путь к ней. Если атрибут path отсутствует, настройки безопасности применяются к той папке, в которой находится файл web.config , со всеми её подпапками.

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

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

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

    Существует несколько специальных символов, обозначающих часто используемые группы. Мы уже видели символ «* », обозначающий всех пользователей.

    Символ «? » обозначает анонимного (не успевшего зарегистрироваться ) пользователя. Несколько групп или отдельных пользователей могут быть перечислены через запятую. Пользователи и роли могут смешиваться в одном правиле, например:

    Защита MVC-сайта

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

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

    Public class AdminController: Controller { ...

    Для обозначения анонимов и всех пользователей доступны те же символы «? » и «* ». Вы можете применять установки ко всему контроллеру или к отдельным действиям. При этом настройки действий будут иметь более высокий приоритет, чем настройки всего контроллера:

    Public ActionResult AdminView() { ...

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

    В ASP.NET 4 был добавлен атрибут , который позволил разрешить анонимному пользователю доступ к отдельным действиям защищённого контроллера.

    Управление контентом в зависимости от роли

    Ограничив доступ к страницам и контроллерам, следующим шагом вы должны убедиться в том, что доступ к самому серверному коду осуществляется правильно. Некоторые объекты легко защитить, потому что к ним должны иметь доступ только пользователи с определённой ролью, и доступ этот – полный.

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

    Например, не имеет смысла показывать ссылку на панель администрирования рядовым пользователям. Клиент, не имеющий отправленных заказов, не должен видеть кнопку трекинга. Даже если элемент не активен или ведёт на страницу авторизации, он может смутить простого пользователя и дать злоумышленнику пищу для размышления.

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

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

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

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

    UpdateOrder.aspx?order=33&action=delete

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

    В другом случае отсутствие проверки в чём-то вроде:

    UpdateOrder.aspx?order=33&action=refund

    позволит злоумышленнику получить возмещение за чужой или не оплаченный заказ. Никогда не полагайтесь на скрытие ссылок как единственный механизм защиты от не авторизованного доступа.

    Аспекты безопасности пользовательских сессий

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

    В этом примере время жизни сессии составит 30 минут. Атрибут slidingExpiration определяет, сбрасывает ли счётчик истечения сессии поступление запроса от пользователя. Если установить данному атрибуту значение false , пользователю придётся осуществлять вход каждые 30 минут, даже во время активной работы с сайтом.

    Также необходимо учитывать возможность кражи сессии. Большинство веб-фреймворков хранят идентификатор сессии в маленьком кусочке текстовых данных, называемом кукой или печенькой (cookie ), в браузере пользователя.

    Если кука никак не защищена, её может использовать злоумышленник, перехватив трафик легитимного пользователя и представившись системе этим пользователем.