Использование фильтров

77

Фильтры внедряют дополнительную логику в обработку запросов инфраструктурой MVC Framework. Они предоставляют простой и элегантный путь реализации сквозной ответственности. Это понятие применяется к функциональности, которая используется по всему приложению и не может быть сосредоточена в одном месте, поскольку нарушился бы шаблон проектирования Separation of Concerns (Разделение ответственности). Классическими примерами сквозной ответственности являются регистрация в журналах, авторизация и кеширование.

Пример приложения

Для этой и последующих статей, посвященных фильтрам ASP.NET MVC, был создан новый проект MVC по имени Filter с использованием шаблона Empty (Пустой) и отметкой флажка MVC в разделе Add folders and core referees for (Добавить папки и основные ссылки). Затем создается контроллер Home, который имеет метод действия Index:

using System.Web.Mvc;

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

Здесь внимание будет сосредоточено только на контроллерах, поэтому из методов действий возвращаются строковые значения, а не объекты ActionResult. Это приводит к тому, что инфраструктура MVC Framework отправляет такие строковые значения непосредственно браузеру, минуя механизм визуализации Razor.

Позже будет демонстрироваться применение нового средства MVC, называемого фильтрами аутентификации, для которого необходимо иметь возможность выполнения простой аутентификации пользователей. Поэтому нужно определить статические пользовательские учетные данные в файле Web.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  ...
  <system.web>
    <compilation debug="true" targetFramework="4.5.1" />
    <httpRuntime targetFramework="4.5.1" />
    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login" timeout="2400">
        <credentials passwordFormat="Clear">
          <user name="user"  password="12345"/>
          <user name="admin" password="12345" />
        </credentials>
      </forms>
    </authentication>
  </system.web>
</configuration>

Для простоты определяются два пользователя, user и admin, которым назначен один и тот же пароль. Здесь снова применяется аутентификация с помощью Форм, а посредством атрибута loginUrl указано, что запросы, не прошедшие аутентификацию, должны быть перенаправлены на URL вида /Account/Login. В примере ниже показано содержимое контроллера Account, добавленного в проект, действие Login которого будет целевым в стандартной конфигурации маршрутизации:

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

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

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

Чтобы создать представление для ввода учетных данных, создайте папку Views/Shared, щелкните на ней правой кнопкой мыши и выберите в контекстном меню пункт Add --> MVC 5 View Page (Razor) (Добавить --> Страница представления MVC 5 (Razor)). Укажите в качестве имени Login.cshtml и щелкните на кнопке OK для создания файла представления. Приведите содержимое файла в соответствие с примером:

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title></title>
</head>
<body>
    @using (Html.BeginForm()) {
        @Html.ValidationSummary()
        <p><label>Логин:</label><input name="username" /></p>
        <p><label>Пароль:</label><input name="password" type="password" /></p>
        <input type="submit" value="Войти" />
    }
</body>
</html>

Разделяемое представление было создано из-за того, что позже будет добавлен второй контроллер аутентификации, в котором данное представление также будет задействовано.

Работа с фильтрами

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

namespace GameStore.WebUI.Controllers
{
    public class AdminController : Controller
    {
        // ...

        public ViewResult Index()
        {
            if (!Request.IsAuthenticated)
            {
                FormsAuthentication.RedirectToLoginPage();
            }
            // ... остальная часть метода действия
        }

        public ViewResult Edit(int gameId)
        {
            if (!Request.IsAuthenticated)
            {
                FormsAuthentication.RedirectToLoginPage();
            }
            // ... остальная часть метода действия
        }

        public ActionResult Edit(Game game, HttpPostedFileBase image = null)
        {
            if (!Request.IsAuthenticated)
            {
                FormsAuthentication.RedirectToLoginPage();
            }
            // ... остальная часть метода действия
        }

        // ... другие методы действий
    }
}

Как видите, при таком подходе появляется множество повторяющегося кода, поэтому было принято решение взамен использовать фильтр:

namespace GameStore.WebUI.Controllers
{
    [Authorize]
    public class AdminController : Controller
    {
        // ...
    }
}

Фильтры - это атрибуты .NET, которые добавляют дополнительные шаги в процесс обработки запросов. В примере выше используется фильтр Authorize, который обеспечивает тот же самый эффект, что и весь дублированный код проверки в примере еще выше.

Введение в типы фильтров

Инфраструктура MVC Framework поддерживает фильтры пяти типов. Каждый тип фильтра позволяет внедрять логику в ту или иную точку процесса обработки запросов. Типы фильтров описаны в таблице ниже:

Типы фильтров MVC Framework
Тип фильтра Интерфейс Стандартная реализация Описание
Фильтр аутентификации IAuthenticationFilter Нет

Запускается первым, перед любыми другими фильтрами или методом действия, но может быть запущен снова после фильтров авторизации

Фильтр авторизации IAuthorizationFilter AuthorizeAttribute

Запускается вторым, после аутентификации, но перед любыми другими фильтрами или методом действия

Фильтр действия IActionFilter ActionFilterAttribute

Запускается до и после метода действия

Фильтр результата IResultFilter ResultFilterAttribute

Запускается до и после выполнения результата действия

Фильтр исключения IExceptionFilter HandleErrorAttribute

Запускается только в случае, если другой фильтр, метод действия или результат действия сгенерировал исключение

Перед тем, как инфраструктура MVC Framework вызовет действие, она инспектирует определение метода на предмет наличия атрибутов, которые реализуют интерфейсы, перечисленные в таблице. Если атрибуты есть, то в соответствующей точке процесса обработки запросов вызываются методы, определенные этими интерфейсами. Инфраструктура включает стандартные классы атрибутов, которые реализуют интерфейсы фильтров. Мы покажем, как использовать эти стандартные классы в последующих статьях.

Класс ActionFilterAttribute реализует интерфейсы IActionFilter и IResultFilter. Это абстрактный класс, который вынуждает предоставлять конкретную реализацию. Классы AuthorizeAttribute и HandleErrorAttribute содержат полезные средства и могут использоваться без создания производных классов.

Применение фильтров к контроллерам и методам действий

Фильтры могут применяться к индивидуальным методам действий или ко всему контроллеру. В примере ранее фильтр Authorize применялся к классу AdminController, и это давало тот же самый эффект, что и его применение к каждому методу действия в данном контроллере:

namespace GameStore.WebUI.Controllers
{
    public class AdminController : Controller
    {
        // ...
        
        [Authorize]
        public ViewResult Index()
        {
            // ... остальная часть метода действия
        }

        [Authorize]
        public ViewResult Edit(int gameId)
        {
            // ... остальная часть метода действия
        }

        [Authorize]
        public ActionResult Edit(Game game, HttpPostedFileBase image = null)
        {
            // ... остальная часть метода действия
        }

        // ... другие методы действий
    }
}

Можно применять множество фильтров, смешивая их на самых разных уровнях, причем совершенно не важно, применяются они к контроллеру или к отдельному методу действия. В примере ниже демонстрируется использование трех разных фильтров:

namespace GameStore.WebUI.Controllers
{
    [Authorize(Roles="admin")]                    // Применяется ко всем методам действий
    public class AdminController : Controller
    {
        [ShowMessage]
        [OutputCache(Duration=60)]                // Применяется только к этому действию
        public ViewResult Index()
        {
            // ...
        }
        
        // ...
    }
}

Некоторые фильтры в этом коде принимают параметры. По мере рассмотрения различных видов фильтров будет объяснено, как они работают.

Стоит заметить также, если вы определили специальный базовый класс для своих контроллеров, то любые фильтры, применяемые к базовому классу, будут воздействовать также и на производные от него классы.

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