Как настроить фильтры "до" и "после"

Как настроить фильтры "до" и "после"

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

Некоторые веб-фреймворки определяют методы вроде preExecute() и postExecute(), но в Symfony такого нет. Хорошая новость заключается в том, что для вмешательства в процесс Запрос -> Ответ существует способ лучше, с использованием компонента EventDispatcher.

Пример валидации токена

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

Итак, перед тем, как выполнять действие вашего контроллера, вам нужно проверить, ограничено ли оно. Если нет, то вам нужно валидировать предоставленный токен.

Note

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

Фильтры "до" с событием kernel.controller

Для начала, определите некоторую конфигурацию токена в качестве параметров:

  • YAML
  • XML
  • PHP
1
2
3
4
5
# config/services.yaml
parameters:
    tokens:
        client1: pass1
        client2: pass2

Тегируйте контроллеры для проверки

Слушатель kernel.controller (он же KernelEvents::CONTROLLER) получает уведомления на каждый запрос, прямо перед тем, как выполняется контроллер. Так что для начала, вам нужно каким-то образом указать, нужна ли валидация токена контроллеру, совпадающему с запросом.

Простым и чистым путём будет создать пустой интерфейс и заставить контроллера реализовать его:

1
2
3
4
5
6
namespace App\Controller;

interface TokenAuthenticatedController
{
    // ...
}

Контроллер, реализующий этот интерфейс, выглядит просто так:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace App\Controller;

use App\Controller\TokenAuthenticatedController;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class FooController extends Controller implements TokenAuthenticatedController
{
    // Действие, требующее аутентификации
    public function bar()
    {
        // ...
    }
}

Создаие подписчика событий

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/EventSubscriber/TokenSubscriber.php
namespace App\EventSubscriber;

use App\Controller\TokenAuthenticatedController;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;

class TokenSubscriber implements EventSubscriberInterface
{
    private $tokens;

    public function __construct($tokens)
    {
        $this->tokens = $tokens;
    }

    public function onKernelController(FilterControllerEvent $event)
    {
        $controller = $event->getController();

        /*
         * переданный $controller может быть либо классом, либо Closure.
         * Это необычно для Symfony, но может случаться.
         * Если это класс, то он представлен в формате массива
         */
        if (!is_array($controller)) {
            return;
        }

        if ($controller[0] instanceof TokenAuthenticatedController) {
            $token = $event->getRequest()->query->get('token');
            if (!in_array($token, $this->tokens)) {
                throw new AccessDeniedHttpException('This action needs a valid token!');
            }
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            KernelEvents::CONTROLLER => 'onKernelController',
        );
    }
}

Вот и всё! Ваш файл services.yaml должен быть уже установлен для загрузки сервисов из каталога EventSubscriber. Symfony заботится об остальном. Ваш метод TokenSubscriber onKernelController() будет выполнен при каждом запросе. Если контроллер, который должен быть вот-вот выполнен, реализует токен TokenAuthenticatedController, то применяется аутентификация токена. Это позволяет вам иметь фильтр "до" в любом желаемом вами контроллере.

Tip

Если ваш подписчик не вызывается при каждом запросе, перепроверьте, загружаете ли вы сервисы из каталога EventSubscriber и включена ли автоконфигурация . Вы также можете вручную добавить тег kernel.event_subscriber.

Фильтры "после" с событием kernel.response

В дополнение к "крючку", который выполняется перед вашим контроллером, вы также можете добавить крючок, который будет выполняться после его. Для этого примера, представьте, что вы хотите добавить хеш sha1 (с солью, использующей этот токен) ко всем ответам, которые прошли аутентификацию этого токена.

Ещё одно основное событие Symfony, по имени kernel.response (оно же KernelEvents::RESPONSE) - уведомляется при каждом запросе, но после того, как контроллер возвращает объект Response. Создание слушателя "после" так же легко, как создание класса слушателя и регистрирование его в качестве сервиса в этом событии.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function onKernelController(FilterControllerEvent $event)
{
    // ...

    if ($controller[0] instanceof TokenAuthenticatedController) {
        $token = $event->getRequest()->query->get('token');
        if (!in_array($token, $this->tokens)) {
            throw new AccessDeniedHttpException('This action needs a valid token!');
        }

        // отметьте запрос как такой, что прошёл аутентификацию токена
        $event->getRequest()->attributes->set('auth_token', $token);
    }
}

Теперь, сконфигурируйте подписчика так, чтобы он слушал другое событие, и добавьте onKernelResponse(). Он будет искать отметку auth_token в объекте запроса и устанавливать пользовательский заголовок в ответе, если она будет найдена:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// добавьте новое утверждение использования наверху в вашем файле
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

public function onKernelResponse(FilterResponseEvent $event)
{
    // проверьте, отметил ли onKernelController это как токен запроса "auth'ed"
    if (!$token = $event->getRequest()->attributes->get('auth_token')) {
        return;
    }

    $response = $event->getResponse();

    // создайте хеш и установите его, как заголовок ответа
    $hash = sha1($response->getContent().$token);
    $response->headers->set('X-CONTENT-HASH', $hash);
}

public static function getSubscribedEvents()
{
    return array(
        KernelEvents::CONTROLLER => 'onKernelController',
        KernelEvents::RESPONSE => 'onKernelResponse',
    );
}

Вот и всё! TokenSubscriberтеперь будет уведомлён перед тем, как будет выполнен каждый контроллер (onKernelController()) и после того, как каждый контроллер вернёт ответ (onKernelResponse()). Заставляя конкретные контроллеры реализовывать интерфейс TokenAuthenticatedController, ваш слушатель знает, с какими контроллерами ему нужно работать. А сохраняя значение в пакете запроса "атрибуты", метод onKernelResponse() знает, что нужно добавить дополнительный заголовок. Повеселитесь!