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

Скажем, что у вас есть.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; }

Сергей Бакланов

Imports System.Data.SqlClient Imports System.Web.Security Public Class login Inherits System.Web.UI.Page Protected WithEvents txtName As System.Web.UI.WebControls.TextBox Protected WithEvents txtPassword As System.Web.UI.WebControls.TextBox Protected WithEvents lbl As System.Web.UI.WebControls.Label Protected WithEvents btnLogin As System.Web.UI.WebControls.Button #Region " Web Form Designer Generated Code " "This call is required by the Web Form Designer. Private Sub InitializeComponent() End Sub "NOTE: The following placeholder declaration is required by the Web Form Designer. "Do not delete or move it. Private designerPlaceholderDeclaration As System.Object Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init "CODEGEN: This method call is required by the Web Form Designer "Do not modify it using the code editor. InitializeComponent() End Sub #End Region Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load "Put user code to initialize the page here End Sub Private Sub btnLogin_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnLogin.Click Dim cn As New SqlConnection("server=localhost;database=FormAuthUsers;uid=sa;pwd=;") Dim cm As New SqlCommand("FindUser", cn) Dim dr As SqlDataReader Dim ticket As FormsAuthenticationTicket Dim n As Integer, strRoles As String, strEncrypted As String " Открываем соединение Try cn.Open() Catch ex As SqlException Response.Write(ex.Message) Exit Sub End Try " Задаём тип команды cm.CommandType = CommandType.StoredProcedure " Добавляем параметры имени Dim prmName = New SqlParameter("@Name", SqlDbType.NVarChar, 50) prmName.Value = txtName.Text cm.Parameters.Add(prmName) " Добавляем параметр пароля Dim prmPass = New SqlParameter("@Password", SqlDbType.NVarChar, 50) prmPass.Value = txtPassword.Text cm.Parameters.Add(prmPass) " Выполняем запрос n = cm.ExecuteScalar If n > 0 Then " Если пользователь с таким именем и паролем существует, то ищем его роли cm = Nothing cm = New SqlCommand("exec FindRoles "" & txtName.Text & """, cn) dr = cm.ExecuteReader() " Составляем список ролей While dr.Read If strRoles = "" Then strRoles &= dr(0) Else strRoles &= ", " & dr(0) End If End While " Создаём аутентификационный билет ticket = New FormsAuthenticationTicket(1, txtName.Text, DateTime.Now, _ DateTime.Now.AddMinutes(20), False, strRoles) " Шифруем билет strEncrypted = FormsAuthentication.Encrypt(ticket) " Сохраняем cookie-файл Response.Cookies.Add(New HttpCookie("UrlAuthz", strEncrypted)) " Возвращаемся на исходную страницу FormsAuthentication.RedirectFromLoginPage(txtName.Text, False) Else " Если пользователь не был найден, то выдаём сообщение об ошибке lbl.Visible = True End If End Sub End Class

В этом примере мы поместили в одну процедуру две операции проверки: одна - аутентификации, другая - авторизации. Сначала мы проходим аутентификацию, запрашивая данные на пользователя с таким-то именем и паролем из базы. Если пользователь не был найден, выводим соответствующее сообщение об ошибке (см. 4 строку снизу). Если же пользователь обнаружен, то мы определяем его роли, опять запрашивая информацию из базы данных. На основе полученных сведений о ролях формируется аутентификационный билет, который впоследствии шифруется и сохраняется в cookie-файле. И, наконец, пользователь благополучно возвращается на страницу default.aspx.

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

Листинг 7. default.aspx

AuthzByUrl Sub Page_Load(sender As Object, e As EventArgs) Handles MyBase.Load If HttpContext.Current.User.Identity.Name = "" Then lblLogin.Text = "You"re not registered, please login" Else lblLogin.Text = "You"re registered as " & _ HttpContext.Current.User.Identity.Name End If End Sub Sub btnLogin_Click(sender As Object, e As EventArgs) Handles btnLogin.Click Response.Redirect("login.aspx") End Sub You"re not registered, please login
GoTo:
  • Admin zone
  • User zone

admin.aspx

admin

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

Заимствование полномочий

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

По умолчанию режим заимствования полномочий в среде ASP.NET отключён. Для его активизации нужно добавить в файл Web.config тэг и присвоить его атрибуту impersonate значение true. Следующий фрагмент файла конфигурации проекта демонстрирует, как это должно выглядеть:

Web.config

Для демонстрации работы этого режима, используйте следующий код (листинг 8) в странице default.aspx:

default.aspx

Impersonation User: IsAuthenticated Authentication type Name WindowsIdentity: IsAuthenticated Authentication type Name

default.aspx.vb

Imports System.Security.Principal Public Class WebForm1 Inherits System.Web.UI.Page #Region " Web Form Designer Generated Code " "This call is required by the Web Form Designer. Private Sub InitializeComponent() End Sub "NOTE: The following placeholder declaration is required by the Web Form Designer. "Do not delete or move it. Private designerPlaceholderDeclaration As System.Object Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init "CODEGEN: This method call is required by the Web Form Designer "Do not modify it using the code editor. InitializeComponent() End Sub #End Region Protected WithEvents clmIsAuthU As System.Web.UI.HtmlControls.HtmlTableCell Protected WithEvents clmAuthTypeU As System.Web.UI.HtmlControls.HtmlTableCell Protected WithEvents clmNameU As System.Web.UI.HtmlControls.HtmlTableCell Protected WithEvents clmIsAuthW As System.Web.UI.HtmlControls.HtmlTableCell Protected WithEvents clmAuthTypeW As System.Web.UI.HtmlControls.HtmlTableCell Protected WithEvents clmNameW As System.Web.UI.HtmlControls.HtmlTableCell Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim wi As WindowsIdentity " User.Identity With context.User.Identity clmIsAuthU.InnerText = .IsAuthenticated.ToString clmAuthTypeU.InnerText = .AuthenticationType.ToString clmNameU.InnerText = .Name End With " System.Security.Principal.WindowsIdentity wi = WindowsIdentity.GetCurrent With wi clmIsAuthW.InnerText = .IsAuthenticated.ToString clmAuthTypeW.InnerText = .AuthenticationType.ToString clmNameW.InnerText = .Name End With End Sub End Class

В обработчике события загрузки формы для получения идентификатора пользователя объекта WindowsIdentity используется метод GetCurrent, возвращающий идентификатор учётной записи, от имени которой функционирует процесс ASP.NET.

При запуске этого приложения с отключенным заимствованием полномочий () перед вами появится экран, представленный на рисунке 3. Как можно наблюдать, при отключенном заимствовании полномочий в объекте WindowsIdentity содержится идентификатор системного пользователя ASPNET.

Теперь, если вы активизируете заимствование полномочий, то увидите результат, представленный в таблице 1.

Таблица 1. Включенное заимствование полномочий и отключенный анонимный доступ

WindowsIdentity:

IsAuthenticated True
Authentication type Negotiate
Name BIGDRAGON\B@k$
IsAuthenticated True
Authentication type NTLM
Name BIGDRAGON\B@k$

Как видите, результаты одинаковые, поскольку оба объекта получают информацию о текущем пользователе. Но предыдущие два примера были ориентированы на условия с запрещённым анонимным доступом для аутентификации средствами Windows. Если разрешить анонимный доступ к приложению, то объект User.Identity не вернёт никакого имени пользователя, а его свойство IsAuthenticated будет иметь значение False. В этом нет ничего удивительного, т. к. если в системе аутентификации Windows разрешён анонимный доступ, то пользователь работает анонимно, то есть не проходит аутентификацию.

В это же время у объекта WindowsIdentity свойство IsAuthenticated будет иметь значение True, а в качестве имени пользователя будет стоять строка следующего формата: IUSR_ , как показано в таблице 2.

Таблица 2. Заимствование полномочий и анонимный доступ разрешены

WindowsIdentity:

IsAuthenticated False
Authentication type
Name
IsAuthenticated True
Authentication type NTLM
Name BIGDRAGON\IUSR_BIGDRAGON

Свойство name объекта WindowsIdentity имеет такое значение потому, что оно возвращает идентификатор пользователя, под которым работает процесс ASP.NET, а не пользователь Web-узла. А поскольку процесс не может работать анонимно, то он получает имя от IIS, если его невозможно получить от текущего пользователя.

Если вы были внимательны при выполнении операций по разрешению/запрету анонимного доступа, то могли заметить, что в поле Имя пользователя была как раз подставлена строка вышеуказанного формата: IUSR_ (рис. 4).

Рисунок 4. В поле Имя пользователя содержится строка, определяющая имя процесса ASP.NET при анонимном доступе

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

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

Web.config :

После запуска тестового приложения с такой конфигурацией на выполнение, состояние объекта User.Identity останется неизменным, а вот в свойстве name объекта WindowsIdentity вместо строки формата IUSR_ появится имя, указанное в атрибуте userName тэга из файла конфигурации проекта, как показано в таблице 3.

Таблица 3. Процесс ASP.NET работает от имени конкретного пользователя

WindowsIdentity:

IsAuthenticated False
Authentication type
Name
IsAuthenticated True
Authentication type NTLM
Name BIGDRAGON\AlBa

Если вы отмените анонимный доступ, то объект User.Identity будет содержать идентификатор зарегистрированного пользователя, а в объекте WindowsIdentity по-прежнему будет оставаться имя пользователя, переданное через атрибут userName.

На этом закончим изучение авторизации как средства безовасности среды ASP.NET. Дальнейшее изучение механизма авторизации требует изучения средств авторизации Windows. Среди них можно выделить списки контроля доступа на низком и высоком уровне, контроль доступа архитектуры клиент/сервер, ролевая безопасность Windows и так далее.

Если эта тема вас действительно заинтересовала, то вы можете найти массу материала в библиотеке MSDN:

  • Темы безопасности в рамках ASP.NET доступны в следующей ветке библиотеки MSDN: .NET Development/.NET Security;
  • По вопросам безопасности всей системы в целом следует обращаться к разделу Security/Security (General)/SDK Documentation.

Если у вас нет MSDN-библиотеки, то к её самому свежему изданию можно обратиться через интернет по адресу: http://msdn.microsoft.com/library/ .

В третьей, заключительной части данной статьи, мы рассмотрим очень актуальную и интересную тему - криптографию. Кроме теории и алгоритмов криптографии мы рассмотрим с различных сторон средства шифрования, предоставляемые платформой.NET Framework, и создадим простенький метод шифрования.

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

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

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

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

В ASP.NET MVC 5 в очередной раз изменился механизм работы с авторизацией и аутентификацией. Теперь для этих задач используется OWIN (The Open Web Interface for .NET) - это спецификация определяющая интерфейс и описывающая взаимодействие между всеми компонентами. Короче говоря, это абстракция (middleware), которая отделяет от приложения обязанности аутентификации.

Благодаря OWIN, из коробки в ASP.NET MVC 5 появилось довольно большое количество крутых плюшек. Например, вы сразу же можете использовать аутентификацию через facebook или google. Кроме того, можно довольно просто сделать аутентификацию и через vkontakte.

Однако с OWIN есть и некоторые проблемы. Главная из них заключается в том, что этот механизм довольно новый, и в сети доступно мало примеров. Совсем не понятно, как расширить стандартного User-а, как изменить названия таблиц AspNetUsers, AspNetUserRoles, AspNetUserLogins, AspNetUserClaims, AspNetRoles, которые создаются по умолчанию, как расширить класс ролей (да и вообще, как работать с ролями — ведь изначально такого примера нет).

Я сам начал искать ответы на данные вопросы. Спустя некоторое время, нашёл цикл статей, описывающих ответы на вопросы, поставленные выше.

— базовая работа с аутентификацией.

— расширение Пользовательского класса, а также первичная работа с Ролями

— Расширение стандартного класса Ролей

  • 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" })) { Вход Email @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") Войти }

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

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



    Есть вопросы?

    Сообщить об опечатке

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