Пользовательский шаблон ошибки 404 в twig 2.5 с Symfony 4.1

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

Он работает, как и ожидалось, но для 404.

В меню навигации моего базового макета у меня есть много is_granted('SOME_ROLES') для отображения доступных разделов сайта в зависимости от прав пользователя. Когда выдается ошибка 404, меню навигации отображается так, как будто пользователь отключен: {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %} является ложным.

После некоторых поисков я обнаружил, что маршрутизатор выполняется раньше, чем брандмауэр. Поскольку при выдаче ошибки 404 маршрут не найден, брандмауэр не выполняется и права не отправляются в шаблон.

Единственный обходной путь, который я нашел (источник от 2014 года), — это добавить в в самом низу файла route.yaml это определение маршрута:

pageNotFound:
    path: /{path}
    defaults:
        _controller: App\Exception\PageNotFound::pageNotFound

Поскольку все остальные маршруты не совпадают, этот должен быть не найден.

Контроллер:

<?php

namespace App\Exception;

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class PageNotFound
{
    public function pageNotFound()
    {
        return (new NotFoundHttpException());
    }
}

Поскольку выполняется контроллер, выполняется брандмауэр, и отображается страница ошибки 404, как я и ожидал (ура!).

Мой вопрос: есть ли какой-нибудь правильный способ исправить эту проблему вместо этого обходного пути?


person Cid    schedule 26.09.2018    source источник


Ответы (1)


У нас была аналогичная проблема.

  • Мы хотели иметь доступ к токену аутентификации на страницах ошибок.
  • В сценарии, где часть веб-сайта находится за брандмауэром, скажем, example.com/supersecretarea/, мы хотели, чтобы неавторизованные пользователи получали код ошибки 403 при доступе к любому URL-адресу за example.com/supersecretarea/, даже если страница не существует. Поведение Symfony не позволяет этого и проверяет 404 (либо из-за отсутствия маршрута, либо из-за того, что параметр маршрута не разрешен, например example.com/supersecretarea/user/198, когда нет пользователя 198).

В итоге мы переопределили маршрутизатор по умолчанию в Symfony (Symfony\Bundle\FrameworkBundle\Routing\Router), чтобы изменить его поведение:

public function matchRequest(Request $request): array
{
    try {
        return parent::matchRequest($request);
    } catch (ResourceNotFoundException $e) {
        // Ignore this next line for now
        // $this->targetPathSavingStatus->disableSaveTargetPath();
        return [
            '_controller' => 'App\Controller\CatchAllController::catchAll',
            '_route' => 'catch_all'
        ];
    }
}

CatchAllController просто отображает страницу ошибки 404:

public function catchAll(): Response
{
    return new Response(
        $this->templating->render('bundles/TwigBundle/Exception/error404.html.twig'),
        Response::HTTP_NOT_FOUND
    );
}

Что происходит, так это то, что во время обычного процесса маршрутизатора Symfony, если что-то должно вызвать ошибку 404, мы перехватываем это исключение в функции matchRequest. Эта функция должна возвращать информацию о том, какое действие контроллера нужно выполнить для рендеринга страницы, вот что мы делаем: мы сообщаем маршрутизатору, что мы хотим рендерить страницу 404 (с кодом 404). Вся безопасность обрабатывается между возвратом matchRequest и вызовом catchAll, поэтому брандмауэры могут вызывать ошибки 403, у нас есть токен аутентификации и т. д.


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

// Symfony\Component\Security\Http\Firewall\ExceptionListener
protected function setTargetPath(Request $request)
{
    // session isn't required when using HTTP basic authentication mechanism for example
    if ($request->hasSession() && $request->isMethodSafe(false) && !$request->isXmlHttpRequest()) {
        $this->saveTargetPath($request->getSession(), $this->providerKey, $request->getUri());
    }
}

Но теперь, когда мы позволяем несуществующим страницам запускать перенаправления брандмауэра на страницу входа (скажем, example.com/registered_users_only/* перенаправляет на страницу загрузки, а пользователь, не прошедший проверку подлинности, нажимает на example.com/registered_users_only/page_that_does_not_exist), мы абсолютно не хотим сохранять эту несуществующую страницу как новый «TargetPath» для перенаправления после успешного входа в систему, иначе пользователь увидит, казалось бы, случайную ошибку 404. Мы решили расширить setTargetPath прослушивателя исключений и определили службу, которая переключает, должен ли прослушиватель исключений сохранять целевой путь или нет.

// Our extended ExceptionListener
protected function setTargetPath(Request $request): void
{
    if ($this->targetPathSavingStatus->shouldSave()) {
        parent::setTargetPath($request);
    }
}

Это цель закомментированной строки $this->targetPathSavingStatus->disableSaveTargetPath(); сверху: отключить состояние по умолчанию, чтобы сохранить целевой путь в исключениях брандмауэра, когда есть 404 (переменные targetPathSavingStatus здесь указывают на очень простую службу, используемую только для хранения этой части информации).

Эта часть решения не очень удовлетворительна. Я хотел бы найти что-то лучше. Тем не менее, похоже, что на данный момент он выполняет свою работу.

Конечно, если у вас от always_use_default_target_path до true, то конкретно это исправление не нужно.


ИЗМЕНИТЬ:

Чтобы Symfony использовал мои версии Router и Exception listener, я добавил следующий код в метод process() Kernel.php:

public function process(ContainerBuilder $container)
{
    // Use our own CatchAll router rather than the default one
    $definition = $container->findDefinition('router.default');
    $definition->setClass(CatchAllRouter::class);
    // register the service that we use to alter the targetPath saving mechanic
    $definition->addMethodCall('setTargetPathSavingStatus', [new Reference('App\Routing\TargetPathSavingStatus')]);

    // Use our own ExceptionListener so that we can tell it not to use saveTargetPath
    // after the CatchAll router intercepts a 404
    $definition = $container->findDefinition('security.exception_listener');
    $definition->setClass(FirewallExceptionListener::class);
    // register the service that we use to alter the targetPath saving mechanic
    $definition->addMethodCall('setTargetPathSavingStatus', [new Reference('App\Routing\TargetPathSavingStatus')]);

    // ...
}
person NicolasB    schedule 27.09.2018
comment
+1 и спасибо за ваш ответ, это был умный способ переопределить поведение 404. Однако вы изменили файлы поставщиков, не так ли? В случае обновления ваши изменения могут быть снова отменены - person Cid; 02.10.2018
comment
@Cid Нет, извините, если я не был более конкретным, я не делал никаких правок в файлах поставщиков. Я создал свой собственный класс, расширяющий Symfony\Bundle\FrameworkBundle\Routing\Router и Symfony\Component\Security\Http\Firewall\ExceptionListener, а затем использовал definition->setClass в Kernel.php, чтобы Symfony использовал мои сервисы, а не стандартные. Через минуту я обновлю свой ответ кодом. - person NicolasB; 02.10.2018
comment
кто-нибудь нашел более элегантное решение? Спасибо - person zskiredj; 22.07.2020