【问题标题】:Custom 404 error template in twig 2.5 with symfony 4.1带有 symfony 4.1 的 twig 2.5 中的自定义 404 错误模板
【发布时间】:2019-03-02 05:50:41
【问题描述】:

我为 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 错误页面(万岁!)。

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

【问题讨论】:

    标签: php twig http-status-code-404 symfony4 symfony-routing


    【解决方案1】:

    我们遇到了类似的问题。

    • 我们希望能够访问错误页面中的身份验证令牌。
    • 在网站的一部分位于防火墙后面的情况下,比如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 函数中捕获该异常。这个函数应该返回关于运行哪个控制器动作来渲染页面的信息,所以这就是我们所做的:我们告诉路由器我们想要渲染一个 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-&gt;targetPathSavingStatus-&gt;disableSaveTargetPath();行的目的:当有404时,将是否在防火墙异常上保存目标路径的默认开启状态关闭(这里的targetPathSavingStatus变量指向一个非常简单的服务仅用于存储该信息)。

    这部分解决方案不是很令人满意。我想找到更好的东西。不过,它现在似乎确实可以完成这项工作。

    当然,如果您有 always_use_default_target_pathtrue,则无需进行此特定修复。


    编辑:

    为了让 Symfony 使用我的路由器和异常监听器版本,我在 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')]);
    
        // ...
    }
    

    【讨论】:

    • +1 感谢您的回答,这是覆盖 404 行为的明智方法。但是,您已经修改了供应商文件,不是吗?如果更新,您的更改可能会再次被覆盖
    • @Cid 不,很抱歉,如果我没有更具体,我没有对供应商文件进行任何编辑。我创建了自己的扩展Symfony\Bundle\FrameworkBundle\Routing\RouterSymfony\Component\Security\Http\Firewall\ExceptionListener 的类,然后我在Kernel.php 中使用definition-&gt;setClass 来让Symfony 使用我的服务而不是默认服务。我会在一分钟内用代码更新我的答案。
    • 有没有人找到更优雅的解决方案?谢谢
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-18
    • 1970-01-01
    • 2014-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多