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

Дата обновления перевода 2023-09-26

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

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

  • Если пользователь не аутентифицирован (или аутентифицирован анонимно), точка входа аутентификации используется для генерирования ответа (обычно, перенаправления к странице входа в систему, или ответу 401 Неавторизовано;
  • Если пользователь аутентифицирован, но не имеет необходимых разрешений, генерируется ответ 403 Запрещено.

Настройте ответ отказа в авторизации

Вам нужно создать класс, реализующий AuthenticationEntryPointInterface. Этот интерфейс имеет один метод (start()), который вызывается, когда неаутентифицированный пользователь пытается получить доступ к защищенному источнику:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Security/AuthenticationEntryPoint.php
namespace App\Security;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
{
    public function __construct(
        private UrlGeneratorInterface $urlGenerator,
    ) {
    }

    public function start(Request $request, AuthenticationException $authException = null): RedirectResponse
    {
        // добавьте пользовательское флэш-сообщение, и перенаправьте на страницу входа в систему
        $request->getSession()->getFlashBag()->add('note', 'You have to login in order to access this page.');

        return new RedirectResponse($this->urlGenerator->generate('security_login'));
    }
}

Это все, если вы используете конфигурацию services.yaml по умолчанию . Иначе, вам нужно зарегистрировать этот сервис в контейнере.

Теперь, сконфигурируйте этот ID сервиса, в качестве точки входа в файерволл:

1
2
3
4
5
6
7
# config/packages/security.yaml
firewalls:
    # ...

    main:
        # ...
        entry_point: App\Security\AuthenticationEntryPoint

Настройте ответ "запрещено"

Создайте класс, который реализует AccessDeniedHandlerInterface. Этот интерфейс определяет один метод под названием handle(), где вы можете реализовывать любую логику, которую необходимо, когда текущему пользователю в доступе отказано (например, отправить письмо, сделать запись лога сообщения, или просто вернуть пользовательский ответ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Security/AccessDeniedHandler.php
namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;

class AccessDeniedHandler implements AccessDeniedHandlerInterface
{
    public function handle(Request $request, AccessDeniedException $accessDeniedException): ?Response
    {
        // ...

        return new Response($content, 403);
    }
}

Если вы используете конфигурацию services.yaml по умолчанию , вы закончили! Symfony автоматически узнает о вашем сервисе. Затем, вы сможете сконфигурировать его под своим брандмауэром:

1
2
3
4
5
6
7
# config/packages/security.yaml
firewalls:
    # ...

    main:
        # ...
        access_denied_handler: App\Security\AccessDeniedHandler

Настройка всех ответов отказа в доступе

В некоторых случаях, вам может захотеть и настроить ответы, и выполнить какое-то действие (например, вход в систему) для каждого AccessDeniedException. В таком случае, сконфигурируйте слушатель kernel.exception :

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
// src/EventListener/AccessDeniedListener.php
namespace App\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class AccessDeniedListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            // приоритет должен быть больше, чем HTTP Security
            // ExceptionListener, чтобы он вызывался до слушателя
            // исключений по умолчанию
            KernelEvents::EXCEPTION => ['onKernelException', 2],
        ];
    }

    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        if (!$exception instanceof AccessDeniedException) {
            return;
        }

        // ... выполнить какое-то действие (например, запись логов)

        // по желанию, установить пользовательский ответ
        $event->setResponse(new Response(null, 403));

        // или прекратить распространение (предотвращает вызов следующих слушателей исключений)
        //$event->stopPropagation();
    }
}