带有 symfony 4.1 的 twig 2.5 中的自定义 404 错误模板

Custom 404 error template in twig 2.5 with symfony 4.1

我为 http 错误显示创建了自定义 Twig 模板,以通过扩展我的基本布局来保持网站设计的统一。 (我想保留我的导航菜单并显示错误,与常规错误消息不同)

它按预期工作,但对于 404。

在我的基本布局的导航菜单中,我有很多 is_granted('SOME_ROLES') 来根据用户的权限显示网站的可用部分。当抛出 404 时,导航菜单显示为用户断开连接:{% if is_granted("IS_AUTHENTICATED_REMEMBERED") %} 为假。

经过一番搜索,我发现路由器是在防火墙之前执行的。由于在抛出 404 时找不到路由,因此不会执行防火墙并且不会将权限发送到模板。

我找到的唯一解决方法 (source from 2014) 是在 routes.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 错误页面(万岁!)。

我的问题是:是否有任何正确方法来解决该问题而不是解决方法?

我们遇到了类似的问题。

  • 我们希望在错误页面中访问身份验证令牌。
  • 在部分网站位于防火墙后面的情况下,比如 example.com/supersecretarea/,我们希望未经授权的用户在访问 example.com/supersecretarea/ 后面的任何 url 时获得 403 错误代码, 即使页面不存在。 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 函数中捕获该异常。这个函数应该 return 关于 运行 呈现页面的控制器操作的信息,所以这就是我们所做的:我们告诉路由器我们想要呈现 404 页面(使用 404 代码)。所有安全性都在 matchRequest returning 和 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_pathtrue,则不需要此特定修复。


编辑:

为了让 Symfony 使用我的 Router 和 Exception 侦听器版本,我在 Kernel.phpprocess() 方法中添加了以下代码:

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')]);

    // ...
}