Сергей Бакланов
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.aspx
admin |
После создания этого простого Web-узла вы сможете собственными глазами увидеть плоды своего труда. В приведённом коде указаны все инструкции, необходимые для создания действующей системы безопасности для сайта на базе аутентификации формой и авторизации с помощью URL.
Заимствование полномочийЗаимствование полномочий - это такой режим работы, при котором приложение ASP.NET функционирует от имени конкретного пользователя. Казалось бы, какой смыл вводить заимствование полномочий, если при аутентификации Windows пользователь и так заходит под конкретной учётной записью? Но всё дело в том, что идентификатор пользователя при аутентификации и идентификатор пользователя при заимствовании полномочий - это разные вещи, и применяются они соответственно для получения различной информации.
По умолчанию режим заимствования полномочий в среде ASP.NET отключён. Для его активизации нужно добавить в файл Web.config тэг и присвоить его атрибуту impersonate значение true. Следующий фрагмент файла конфигурации проекта демонстрирует, как это должно выглядеть:
Web.config
Для демонстрации работы этого режима, используйте следующий код (листинг 8) в странице default.aspx:
default.aspx
Impersonation
|
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. Включенное заимствование полномочий и отключенный анонимный доступ
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. Заимствование полномочий и анонимный доступ разрешены
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 работает от имени конкретного пользователя
IsAuthenticated | False |
Authentication type | |
Name | |
IsAuthenticated | True |
Authentication type | NTLM |
Name | BIGDRAGON\AlBa |
Если вы отмените анонимный доступ, то объект User.Identity будет содержать идентификатор зарегистрированного пользователя, а в объекте WindowsIdentity по-прежнему будет оставаться имя пользователя, переданное через атрибут userName.
На этом закончим изучение авторизации как средства безовасности среды ASP.NET. Дальнейшее изучение механизма авторизации требует изучения средств авторизации Windows. Среди них можно выделить списки контроля доступа на низком и высоком уровне, контроль доступа архитектуры клиент/сервер, ролевая безопасность Windows и так далее.
Если эта тема вас действительно заинтересовала, то вы можете найти массу материала в библиотеке MSDN:
Если у вас нет MSDN-библиотеки, то к её самому свежему изданию можно обратиться через интернет по адресу: http://msdn.microsoft.com/library/ .
В третьей, заключительной части данной статьи, мы рассмотрим очень актуальную и интересную тему - криптографию. Кроме теории и алгоритмов криптографии мы рассмотрим с различных сторон средства шифрования, предоставляемые платформой.NET Framework, и создадим простенький метод шифрования.
В составе технологии ASP.Net Web Forms имеется набор ЭУ для создания и использования учетных записей пользователей:
Элемент управления Login предоставляет готовый к использованию интерфейс, который запрашивает имя и пароль пользователя. Он включает кнопку с атрибутом CommandName-"Login” для подключения пользователя. При нажатии пользователем на данную кнопку ЭУ автоматически выполняет проверку соответствия введенного имени и пароля пользователя с данными, содержащимися в БД, а затем вызывает переход к запрашиваемой web-форме приложения. Данный ЭУ является полностью расширяемым и позволяет переопределить его разметку, стиль и свойства, а также самому обрабатывать события, чтобы изменить стандартное поведение. Пример настройки описания ЭУ Login показан ниже:
Подключение к системе
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:
…
План таков:
Поехали. Добавляем 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) {
Контроллер входа выхода 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") Войти }Запускаем и проверяем:
Все исходники находятся по адресу