Данная статья скорей всего будет полезна начинающим программистам, использующим ASP.NET MVC в качестве платформы для разработки. Но возможно и «бывалые» (опытные) пользователи подчерпнут для себя какие-нибудь идеи. Критика, предложения и тому подобное приветствуется. Всех заинтересовавшихся прошу под кат.
Итак, нашей целью будет написание настраиваемой (кастомной) аутентификации, с небольшой системой безопасности на основе ролей. Она не будет требовать изменения или дополнения модели предметной области приложения. Основное требование, это какой либо намек на пользователей и систему ролей. Стандартную систему безопасности, основанную на membership провайдерах, мы не будем рассматривать, и тем более использовать. Она мне кажется совсем не удобной. Основной ее минус заключается в необходимости конкретной схемы данных, которую зачастую сложно связать с предметной областью вашего приложения
Немного теории.
Начнем с небольшой теоретической части. В платформе ASP.NET MVC существует несколько видов аутентификации, предоставляемой из коробки.
- Windows Authentication (Аутентификация Windows) – одним из примеров являются пользователи, добавленные в дерево AD. Все проверки делаются непосредственно AD, с помощью IIS, через специальный провайдер. Данная аутентификация часто применяется для корпоративных приложений;
- Password Authentication (аутентификация через Password) – централизованная служба аутентификации, предлагаемая Microsoft;
- Form Authentication (Аутентификация с помощью форм) – данные вид аутентификации подходит для приложений, доступных через Интернет.Она работает через клиентскую переадресацию, на указанную html страницу, с формой авторизации. На форме клиент вводит свои учетные данные и отправляет на сервер, где они обрабатываются специфичной для данного приложения логикой. Именно такой вид аутентификации мы и будем писать.
В аутентификации через форму на стороне клиента хранит зашифрованный cookie-набор. Cookie передается в запросах к серверу, показывая, что пользователь авторизован. Для создания такого зашифрованного набора, в стандартной аутентификации через форму, служит метод Encript в классе System.Web.Security.FormAuthentication. Для декодирования используется метод Decrypt. Чем хорош класс FormAuthentication? Тем, что его методы кодирования и декодирования шифруют и подписывают данные с помощью машинных ключей сервера. Без данных ключей информацию из cookie файла невозможно прочесть или изменить, а нам не нужно изобретать велосипед.
Реализация проекта
Начнем с создания класса идентификации пользователя, который будет доступен через сведения о безопасности текущего Http запроса HttpContext.User.Identity.
[Serializable] //TAccount - Тип аккаунта в бизнес логике. //TRole - Тип роли. public abstract class AbstractIdentity<TAccount, TRole>: MarshalByRefObject, IIdentity { protected AbstractIdentity() { Id = long.MinValue; } private bool _isInitialized = false; public long Id { get; set; } public string Name { get; set; } public string AuthenticationType { get { return String.Format("CustomizeAuthentication_{0}", typeof(TAccount).Name); } } public string[] Role { get; set; } public TRole[] Roles { get; set; } public bool IsAuthenticated { get { return Id != long.MinValue; } } public bool CheckRole(TRole role) { return Role.All(r => r.Equals(role.ToString())); } public void SetAccount(TAccount account) { Id = GetId(account); Name = GetName(account); Roles = GetRole(account); Role = Roles.Select(c=>c.ToString()).ToArray(); InitializeMoreFields(account); _isInitialized = true; } protected virtual void InitializeMoreFields(TAccount account) { } protected abstract long GetId(TAccount account); protected abstract string GetName(TAccount account); protected abstract TRole[] GetRole(TAccount account); public string Serialize() { if (!_isInitialized) throw new AccountNotSetException(); using (var stream = new MemoryStream()) { var formatter = new XmlSerializer(GetType()); formatter.Serialize(stream, this); return Encoding.UTF8.GetString(stream.ToArray()); } } public static TIdenty Deserialize<TIdenty>(string value) where TIdenty : AbstractIdentity<TAccount, TRole> { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(value))) { var formatter = new XmlSerializer(typeof(TIdenty)); return (TIdenty)formatter.Deserialize(stream); } } }
Далее реализуем абстрактный HTTP модуль аутентификации. В котором подписываемся на событя AuthenticateRequest, которое возникает после прохождения проверки подлинсти пользователя. В реализации подписанного метода декадируем cookie, создав и записав пользователя в текщий HTTP запрос.
//TIdenty - Тип уже реализованого клсса идентификации //TAccount - Тип аккаунта в бизнес логике. //TRole - Тип роли. public abstract class AbstractAutentificationModule<TIdenty, TAccount, TRole> : IHttpModule where TIdenty : AbstractIdentity<TAccount, TRole> { public void Init(HttpApplication context) { context.AuthenticateRequest += OnAuthenticateRequest; } private static void OnAuthenticateRequest(object sender, EventArgs e) { var application = (HttpApplication)sender; var context = application.Context; if (context.User != null && context.User.Identity.IsAuthenticated) return; var cookieName = FormsAuthentication.FormsCookieName; var cookie = application.Request.Cookies[cookieName.ToUpper()]; if (cookie == null) return; try { var ticket = FormsAuthentication.Decrypt(cookie.Value); var identity = AbstractIdentity<TAccount, TRole>.Deserialize<TIdenty>(ticket.UserData); var principal = new GenericPrincipal(identity, identity.Role); context.User = principal; Thread.CurrentPrincipal = principal; } catch {} } public void Dispose() {} }
Реализация абстрактного сервиса авторизации. Записывет данные прошедшего авторизацию пользователя в cookie. Дабавляет его в HTTP запрос.
//TAccount - Тип аккаунта в бизнес логике. public interface IAuthorizeService<in TAccount> { void SignIn(TAccount account, bool createPersistentCookie); void SignOut(); } //TIdenty - Тип реализованого класса идентификации //TAccount - Тип аккаунта в бизнес логике. //TRole - Тип роли. public abstract class AbstractAuthorizeService<TIdentity, TAccount, TRole> : IAuthorizeService<TAccount> where TIdentity : AbstractIdentity<TAccount, TRole>, new() { private const int TICKET_VERSION = 1; private const int EXPIRATION_MINUTE = 60; public void SignIn(TAccount account, bool createPersistentCookie) { var accountIdentity = CreateIdentity(account); var authTicket = new FormsAuthenticationTicket(TICKET_VERSION, accountIdentity.Name, DateTime.Now, DateTime.Now.AddMinutes(EXPIRATION_MINUTE), createPersistentCookie, accountIdentity.Serialize()); CreateCookie(authTicket); HttpContext.Current.User = new GenericPrincipal(accountIdentity, accountIdentity.Role); } private TIdentity CreateIdentity(TAccount account) { var accountIdentity = new TIdentity(); accountIdentity.SetAccount(account); return accountIdentity; } private void CreateCookie(FormsAuthenticationTicket ticket) { var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket)) { Expires = DateTime.Now.Add(FormsAuthentication.Timeout), }; HttpContext.Current.Response.Cookies.Add(authCookie); } public void SignOut() { FormsAuthentication.SignOut(); } }
Не забываем про систему безопасности. Для этого реализуем атрибут аутентификации, которому будем передавать роли и правила, необходимые для доступа к методу контролера. Так как атрибуты не поддерживают Generic, делегируем создание правил в другому классу(RuleFactory).
Интерфейс IRule описывает правило проверки аутентификации.
public interface IRule { bool Check(IIdentity user); } //Реализация интерфейса отвечающего за правила //TIdenty - Тип реализованого класса идентификации //TAccount - Тип аккаунта в бизнес логике. //TRole - Тип роли. internal class Rule<TIdentity, TAccount, TRole> : IRule where TIdentity : AbstractIdentity<TAccount, TRole> { private readonly Func<TIdentity, bool> _check; public Rule(Func<TIdentity, bool> check) { if (check == null) throw new ArgumentNullException("check"); _check = check; } public bool Check (IIdentity user) { return _check((TIdentity) user); } } //Фабрика правил //TIdenty - Тип реализованого класса идентификации //TAccount - Тип аккаунта в бизнес логике. //TRole - Тип роли. public class RuleFactory<TIdentity, TAccount, TRole> where TIdentity : AbstractIdentity<TAccount, TRole> { public IRule Create(Func<TIdentity, bool> rule) { return new Rule<TIdentity, TAccount, TRole> (rule); } } //Реализация атрибута public abstract class AbstractAutintificateAttribute : AuthorizeAttribute { private readonly ICollection<IRule> _rules = new List<IRule> (); private readonly bool _isNotSimpleAuthentication; protected AbstractAutintificateAttribute(bool isNotSimpleAuthentication) { _isNotSimpleAuthentication = isNotSimpleAuthentication; } protected void AddRule(IRule rule) { if (rule == null) throw new ArgumentNullException ("rule"); _rules.Add (rule); } protected override bool AuthorizeCore(HttpContextBase httpContext) { if (httpContext == null) throw new ArgumentNullException("httpContext"); if (httpContext.User == null || !httpContext.User.Identity.IsAuthenticated) return false; var isAuthorize = false; isAuthorize |= _rules.Any(rule => rule.Check(httpContext.User.Identity)); isAuthorize |= httpContext.Request.IsAuthenticated && !_isNotSimpleAuthentication; return isAuthorize; } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { var context = filterContext.HttpContext; var appPath = context.Request.ApplicationPath == "/" ? string.Empty : context.Request.ApplicationPath; var loginUrl = FormsAuthentication.LoginUrl; var path = HttpUtility.UrlEncode(context.Request.Url.PathAndQuery); var url = String.Format("{0}{1}?ReturnUrl={2}", appPath, loginUrl, path); if (!filterContext.IsChildAction) filterContext.Result = new RedirectResult(url); } }
Для удобства использования в своих проектах, реализуем базовый абстрактный контроллер. Который будет предоставлять удобный досуп к текушему пользователю.
//TIdenty - Тип реализованого класса идентификации //TAccount - Тип аккаунта в бизнес логике. //TRole - Тип роли. public abstract class AbstractController<TIdenty, TAccount, TRole> : Controller where TIdenty : AbstractIdentity<TAccount, TRole> { protected AbstractController() { _user = new Lazy<TIdenty>(() => HttpContext.User.Identity as TIdenty); } private readonly Lazy<TIdenty> _user; protected TIdenty CurrentUser { get { return _user.Value; } } }
Пример использование
Восновном все реализация для конкретного проекта, заключается в прописание нужных generic типов.
Первое с чего будем начинать во всех проектах, это реализация класса идентификации.
public class ExampleIdentity : AbstractIdentity<Account, Role> { public string Email { get; set; } //Реализация абстрактного класса. возврощает уникальный идентификатор пользователя protected override long GetId(Account account) { return account.Id; } //Реализация абстрактного класса. возврощает имя пользователя(логин) protected override string GetName(Account account) { return account.Login; } //Реализация абстрактного класса. возврощает список ролей пользователя protected override Role[] GetRole(Account account) { return new []{ account.Role }; } //Переопределение метода. Если класс идентификации имеет какие либо дополнительные свойсва. их инициализация происходит сдесь protected override void InitializeMoreFields(Account account) { Email = account.Email; } }
Реализация модуля аутетификации
public class ExampleAutintificateModule : AbstractAutentificationModule<ExampleIdentity, Account, Role> { }
Незабываем добавить вашь модуль авторизации в Web.config, и там же изменить тип аутентификации.
<?xml version="1.0" encoding="utf-8"?> <configuration> .... <system.web> .... <httpModules> <remove name="FormsAuthentication" /> <add name="FormsAuthentication" type="Example.Infrostructure.ExampleAutintificateModule" /> </httpModules> <authentication mode="Forms"> <!--loginUrl путь к странице авторизации --> <forms loginUrl="~/User/Login" timeout="2880" /> </authentication> </system.web> </configuration> <system.webServer> .... <modules runAllManagedModulesForAllRequests="true"> <remove name="FormsAuthentication" /> <add name="FormsAuthentication" type="Example.Infrostructure.ExampleAutintificateModule" /> </modules> </system.webServer>
Пример сервиса авторизации.
public class ExampleAuthorizeService : AbstractAuthorizeService<ExampleIdentity, Account, Role> { }
Базовый контроллер для текущего проекта
public abstract class ExampleController : AbstractController<ExampleIdentity, Account, Role> { }
Ну и реализуем атрибут с правилами аутентификации
//Фабрика правил public class ExampleRuleFactory : RuleFactory<ExampleIdentity, Account, Role> { } //Все правила создаются с помошью фабрики. public class ExampleAuthintificationAtribute : AbstractAutintificateAttribute { private readonly ExampleRuleFactory _ruleFactory = new ExampleRuleFactory(); public ExampleAuthintificationAtribute(params Role[] allowedRole) : base(allowedRole.Any()) { //Пользователь могут попасть на страницу, только если хи роли есть в списке AddRule(_ruleFactory.Create(account => allowedRole.Intersect(account.Roles).Any())); //Если пользователь админ, то он видит все страници AddRule(_ruleFactory.Create(account => account.Roles.Any(c=>c == Role.Admin))); } }
Заключение
Для удобства я создал nugget-package библиотечку, установить которую можно следующей коммандой:
Install-Package MvcCustomizableFormAuthentication
Репозиторий с проектом находится на github, там есть пример рабочего приложения.
Всем спасибо за уделённое время. Жду советов, критики, предложений.
P.S. Незабываем о unit-тестировании.
ссылка на оригинал статьи http://habrahabr.ru/post/174097/
Добавить комментарий