Фильтры аутентификации

188

Фильтры аутентификации появились в версии MVC 5 и предназначены для обеспечения точного контроля над прохождением пользователями аутентификации для контроллеров и действий в приложении.

Фильтры аутентификации обладают относительно сложным жизненным циклом. Они выполняются перед любыми другими фильтрами, что позволяет определить политику аутентификации, которая будет применяться до использования фильтров других типов. Фильтры аутентификации могут также комбинироваться с фильтрами авторизации, чтобы проводить аутентификацию запросов, которые не соблюдают политику авторизации. Кроме того, фильтры аутентификации выполняются после выполнения метода действия, но перед обработкой ActionResult. Далее будут даны объяснения того, как все это работает, и приведены иллюстрирующие примеры.

Интерфейс IAuthenticationFilter

Фильтры аутентификации реализуют интерфейс IAuthenticationFilter, показанный в примере ниже:

namespace System.Web.Mvc.Filters
{
    public interface IAuthenticationFilter
    {
        void OnAuthentication(AuthenticationContext context);
        void OnAuthenticationChallenge(AuthenticationChallengeContext context);
    }
}

Метод OnAuthenticationChallenge() вызывается инфраструктурой MVC Framework всякий раз, когда запрос не прошел аутентификацию или не удовлетворил политикам авторизации для метода действия. Методу OnAuthenticationChallenge() передается экземпляр класса AuthenticationChallengeContext, производного от класса ControllerContext, который был описан ранее и определяет свойства, показанные ниже:

ActionDescriptor

Возвращает объект ActionDescriptor, описывающий метод действия, к которому был применен фильтр

Result

Устанавливает объект ActionResult, который выражает результат запроса аутентификации

Самым важным свойством является свойство Result, поскольку оно позволяет фильтру аутентификации передать объект ActionResult инфраструктуре MVC Framework - процесс, который называется коротким замыканием и вскоре будет описан. Объяснять работу фильтра аутентификации лучше всего на примере.

Наиболее интересный аспект фильтров аутентификации заключается в том, что они позволяют одиночному контроллеру определять методы действий, которые аутентифицируются разными способами, поэтому первым шагом будет добавление нового контроллера, эмулирующего вход в учетную запись Google. В примере ниже представлено определение класса GoogleAccountController:

using System.Web.Mvc;
using System.Web.Security;

namespace Filter.Controllers
{
    public class GoogleAccountController : Controller
    {
        public ActionResult Login()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Login(string username, string password, string returnUrl)
        {
            if (username.EndsWith("@google.com") && password == "12345")
            {
                FormsAuthentication.SetAuthCookie(username, false);
                return Redirect(returnUrl ?? Url.Action("Index", "Home"));
            }
            else
            {
                ModelState.AddModelError("", "Некорректное имя пользователя или пароль");
                return View();
            }
        }
    }
}

Реализация реального входа в учетную запись Google не планируется, как это сделано, например, на сайте катологов онлайн-сервисов http://onservis.ru/, т.к. это привело бы к погружению в дебри аутентификации третьей стороной, что само по себе является отдельной темой. Взамен предпринят трюк, который будет обеспечивать аутентификацию любого имени пользователя, которое закачивается на @google.com, при условии предоставления пароля "12345".

В данный момент контроллер аутентификации Google не привязан к приложению, и именно здесь в игру вступает фильтр аутентификации. Для этого в папке Infrastructure создается файл класса по имени GoogleAuthAttribute.cs, содержимое которого показано в примере ниже. Класс FilterAttribute, от которого унаследован фильтр GoogleAuth, является базовым для всех классов фильтров.

using System;
using System.Web.Mvc;
using System.Web.Mvc.Filters;
using System.Web.Routing;

namespace Filter.Infrastructure
{
    public class GoogleAuthAttribute : FilterAttribute, IAuthenticationFilter
    {
        public void OnAuthentication(AuthenticationContext context)
        {
            // Не реализован
        }

        public void OnAuthenticationChallenge(AuthenticationChallengeContext context)
        {
            if (context.Result == null)
            {
                context.Result = new RedirectToRouteResult(new RouteValueDictionary {
                    {"controller", "GoogleAccount"}, 
                    {"action",  "Login"}, 
                    {"returnUrl", context.HttpContext.Request.RawUrl}
                });
            }
        }
    }
}

Внутри реализации метода OnAuthenticationChallenge() осуществляется проверка того, установлено ли свойство Result аргумента AuthenticationChallengeContext. Это позволяет избежать запроса на аутентификацию, когда фильтр запускается после выполнения метода действия. Пока не обращайте на такую проверку внимания. Ее важность будет объяснена позже.

В этом разделе важно то, что с помощью метода OnAuthenticationChallenge() пользователю предлагается предоставить учетные данные, для чего пользовательский браузер перенаправляется на контроллер GoogleAccount посредством класса RedirectToRouteResult. Фильтры аутентификации могут использовать все типы ActionResult, однако удобные методы для их создания в классе Controller не предусмотрены, поэтому пришлось применять объект RouteValueDictionary, чтобы указать значения сегментов с целью генерации маршрута к методу действия Login().

Реализация аутентификационной проверки

Рассматриваемый пример фильтра аутентификации готов к запрашиванию у пользователей их фиктивных учетных данных для учетных записей Google, и теперь можно привязать оставшиеся аспекты поведения. Перед выполнением любого другого вида фильтрации контроллер вызовет метод OnAuthentication(), давая возможность проводить обширную аутентификационную проверку. Реализация метода OnAuthentication() не является обязательной, но мы собираемся создать ее для проверки того, что работа производится с учетной записью Google.

Методу OnAuthentication() передается экземпляр класса AuthenticationContext, который подобно классу AuthenticationChallengeContext унаследован от ControllerContext. В классе AuthenticationContext также определены свойства ActionDescriptor и Result, а также свойство Principal, которое возвращает объект ActionDescriptor, описывающий метод действия, к которому был применен фильтр.

Если в коде OnAuthentication() устанавливается значение свойства Result объекта контекста, то инфраструктура MVC Framework вызовет метод OnAuthenticationChallenge(). Если в коде OnAuthenticationChallenge() значение свойства Result объекта контекста не устанавливается, то будет выполнен один из методов OnAuthentication().

Метод OnAuthentication() используется для создания результата, который сообщает пользователю об ошибке аутентификации и может затем быть переопределен методом OnAuthenticationChallenge(), чтобы запросить у пользователя учетные данные. Это позволяет обеспечить отправку значащего ответа, даже если запросы на аутентификацию выдаваться не могут (хотя следует отметить, что мне еще не приходилось сталкиваться с ситуацией, когда бы подобное произошло).

В примере ниже приведена реализация метода OnAuthentication(), в которой осуществляется проверка того, что запрос был аутентифицирован с применением любых учетных данных Google:

using System;
using System.Web.Mvc;
using System.Web.Mvc.Filters;
using System.Web.Routing;
using System.Security.Principal;

namespace Filter.Infrastructure
{
    public class GoogleAuthAttribute : FilterAttribute, IAuthenticationFilter
    {
        public void OnAuthentication(AuthenticationContext context)
        {
            IIdentity ident = context.Principal.Identity;
            if (!ident.IsAuthenticated || !ident.Name.EndsWith("@google.com"))
            {
                context.Result = new HttpUnauthorizedResult();
            }
        }

        public void OnAuthenticationChallenge(AuthenticationChallengeContext context)
        {
            if (context.Result == null || context.Result is HttpUnauthorizedResult)
            {
                context.Result = new RedirectToRouteResult(new RouteValueDictionary {
                    {"controller", "GoogleAccount"}, 
                    {"action",  "Login"}, 
                    {"returnUrl", context.HttpContext.Request.RawUrl}
                });
            }
        }
    }
}

Внутри реализации метода OnAuthentication() выполняется проверка, прошел ли запрос аутентификацию, с применением имени пользователя, которое завершается на @google.com. Если запрос не был аутентифицирован или аутентифицирован с использованием другого вида учетных данных, свойству Result объекта AuthenticationContext присваивается новый экземпляр HttpUnauthorizedResult.

Экземпляр класса HttpUnauthorizedResult устанавливается в качестве значения свойства Result для объекта AuthenticationChallengeContext, который передается методу OnAuthenticationChallenge(). Кроме того, этот метод изменен, чтобы запрашивать у пользователя учетные данные, когда это происходит, координируя действия двух методов в рамках фильтра. Следующий шаг заключается в применении фильтра к контроллеру, как показано в примере ниже:

using System.Web.Mvc;
using Filter.Infrastructure;

namespace Filter.Controllers
{
    public class HomeController : Controller
    {
        [Authorize(Users="admin")]
        public string Index()
        {
            return "Это метод действия Index в контроллере Home";
        }

        [GoogleAuth]
        public string List()
        {
            return "Это метод действия List в контроллере Home";
        }
    }
}

В этом примере определен новый метод действия по имени List(), декорированный фильтром GoogleAuth. В результате доступ к методу действия Index() защищается посредством встроенной поддержки аутентификации с помощью форм, а доступ к методу действия List() - через специальную фиктивную систему аутентификации Google.

Чтобы увидеть эффект от всего проделанного, запустите приложение. По умолчанию браузер будет направлен на метод действия Index(), который инициирует стандартную аутентификацию и требует входа с указанием одного из имен пользователей, определенных в файле Web.config. Если затем запросить URL вида /Home/List, то существующие учетные данные будут отклонены и придется проходить аутентификацию с указанием имени пользователя Google.

Комбинирование фильтров аутентификации и авторизации

Фильтры аутентификации и авторизации можно комбинировать в одних и тех же методах действий, сужая область действия политики безопасности. Инфраструктура ASP.NET MVC Framework вызовет метод OnAuthentication() фильтра, как это было в предыдущем примере, и перейдет к выполнению фильтра авторизации, если запрос прошел аутентификационную проверку. Если запрос не прошел фильтр авторизации, то будет вызван метод OnAuthenticationChallenge() фильтра аутентификации, что позволяет предложить пользователю ввести требуемые учетные данные.

В примере ниже демонстрируется комбинирование фильтров GoogleAuth и Authorize для ограничения доступа к действию List контроллера Home:

using System.Web.Mvc;
using Filter.Infrastructure;

namespace Filter.Controllers
{
    public class HomeController : Controller
    {
        // ...

        [GoogleAuth]
        [Authorize(Users="alex@google.com")]
        public string List()
        {
            return "Это метод действия List в контроллере Home";
        }
    }
}

Фильтр Authorize ограничивает доступ к учетной записи alex@google.com. Если метод действия нацелен на другую учетную запись Google, тогда методу OnAuthenticationChallenge() фильтра аутентификации будет передан объект AuthenticationChallengeContext, свойство Result установлено в экземпляр класса HttpUnauthorizedResult (именно поэтому тот же самый класс используется в методе OnAuthentication()).

Фильтры в контроллере Home ограничивают доступ к методу действия Index() пользователем admin, который аутентифицирован с применением AccountController, а к методу действия List() - пользователем alex@google.com, аутентифицированным через контроллер GoogleAccount.

Обработка финального запроса на аутентификацию

Инфраструктура ASP.NET MVC Framework вызывает метод OnAuthenticationChallenge() еще один, финальный раз после того, как метод действия был выполнен, но перед возвращением и выполнением ActionResult. Это предоставляет фильтрам аутентификации возможность отреагировать на факт завершения действия или даже изменить его результат (подобное допускается проделывать с результатами фильтров).

По этой причине в методе OnAuthenticationChallenge() осуществляется проверка свойства Result объекта AuthenticationChallengeContext. Если этого не сделать - пользователю было бы предложено снова предоставить учетные данные, что имеет мало смысла, поскольку к тому времени метод действия уже выполнен.

Единственная причина, которой я объясняю ответ на последний вызов метода - это прояснение аутентификации для запроса, что может оказаться полезным в ситуациях, когда важные методы действий требуют временного поднятия полномочий, которые должны вводиться при каждом выполнении действия. В примере ниже приведена реализация этого средства:

// ...
using System.Web.Security;

namespace Filter.Infrastructure
{
    public class GoogleAuthAttribute : FilterAttribute, IAuthenticationFilter
    {
        public void OnAuthentication(AuthenticationContext context)
        {
            // ...
        }

        public void OnAuthenticationChallenge(AuthenticationChallengeContext context)
        {
            if (context.Result == null || context.Result is HttpUnauthorizedResult)
            {
                // ...
            }
            else
                FormsAuthentication.SignOut();
        }
    }
}

Чтобы просмотреть окончательный результат, запустите приложение и запросите URL вида Home/List. Будет предложено предоставить учетные данные и в случае аутентификации как alex@google.com появится возможность выполнить соответствующий метод действия. Однако при перезагрузке окна браузера учетные данные буду запрошены повторно, по существу нацеливаясь на метод действия List() во второй раз.

Пройди тесты
Лучший чат для C# программистов