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

в 0:22, , рубрики: .net, asp.net mvc, authentication, c#.net, customization, метки: , , , ,

Привет читатель! Хотел бы поделиться с сообществом своим небольшим опытом разработки на фреймворке 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-тестировании.

Автор: nomit

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js