Настраиваемая авторизация в Asp.Net MVC

от автора

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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *