Как сделать авторизацию 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") Войти }

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

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

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

    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.

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

    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, и создадим простенький метод шифрования.



    
    Top