从Laravel包

时间:2015-11-27 03:13:41

标签: php github laravel-5.1 laravel-routing

坚果壳中的问题

我正在寻找一种从包中删除全局中间件管道中VerifyCsrfToken的方法,而用户不必修改App\Http\Middleware\VerifyCsrfToken。这可能吗?

用例

我正在开发一个软件包,可以轻松地将推送到部署功能安全地添加到任何Laravel项目中。我是从Github开始的。 Github uses webhooks通知第三方应用程序有关事件,例如推送或发布。换句话说,我会在Github上注册一个像http://myapp.com/deploy这样的URL,Github将向该URL发送一个POST请求,其中包含有关该事件的详细信息,并且我可以使用该事件触发新部署。显然,我不希望在Github服务以外的一些随机(或可能是恶意)代理点击该URL的情况下触发部署。因此,Github有a process for securing your webhooks。这涉及到Github注册一个秘密密钥,他们将用它来发送一个特殊的,安全的哈希标头以及你可以用来验证它的请求。

我保证这种安全的方法包括:

随机唯一网址/路由和密钥

首先,我自动生成两个随机的唯一字符串,这些字符串存储在.env文件中,用于在我的应用程序中创建一个密钥路由。在.env文件中,这看起来像:

AUTODEPLOY_SECRET=BHBfCiC0bjIDCAGH2I54JACwKNrC2dqn
AUTODEPLOY_ROUTE=UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx

此软件包的config会创建两个密钥auto-deploy.secretauto-deploy.route,我可以在注册路由时访问这些密钥,以便它永远不会在任何仓库中发布:

Route::post(config('auto-deploy.route'),'MyController@index');

然后我可以去Github并注册我的webook:

Github webhook registration screen

这样,部署URL和用于验证请求的密钥都将保密,并防止恶意代理触发网站上的随机部署。

用于验证Webhook请求的全球中间件

该方法的下一部分涉及为Laravel应用程序创建一个全局中间件,用于捕获和验证webhook请求。我可以使用an approach demonstrated in this Laracasts discussion thread确保我的中间件在队列开头附近执行。在我的包的ServiceProvider中,我可以按如下方式添加一个新的全局中间件类:

public function boot(Illuminate\Contracts\Http\Kernel $kernel)
{
    // register the middleware
    $kernel->prependMiddleware(Middleware\VerifyWebhookRequest::class);
    // load my route
    include __DIR__.'/routes.php';
}

我的Route看起来像是:

Route::post(
    config('auto-deploy.route'), [
        'as' => 'autodeployroute',
        'uses' => 'MyPackage\AutoDeploy\Controllers\DeployController@index',
    ]
);

然后我的中间件会实现一个类似于{/ p>的handle()方法

public function handle($request, Closure $next)
{
    if ($request->path() === config('auto-deploy.route')) {
        if ($request->secure()) {
            // handle authenticating webhook request
            if (/* webhook request is authentic */) {
                // continue on to controller
                return $next($request);
            } else {
                // abort if not authenticated
                abort(403);
            }
        } else {
            // request NOT submitted via HTTPS
            abort(403);
        }
    }
    // Passthrough if it's not our secret route
    return $next($request);
}

此功能一直工作到continue on to controller位。

详细问题

当然这里的问题是,由于这是POST请求,并且没有session()且无法提前获得CSRF令牌,因此全局{{1中间件生成VerifyCsrfToken并中止。我已阅读了大量的论坛帖子,并通过源代码进行了挖掘,但我找不到任何简洁明了的方法来禁用这一请求的TokenMismatchException中间件。我尝试了几种解决方法,但出于各种原因我不喜欢它们。

解决方法尝试#1:让用户修改VerifyCsrfToken中间件

解决此问题的文档化和支持的方法是将URL添加到VerifyCsrfToken类中的$except数组,例如

App\Http\Middleware\VerifyCsrfToken

显然,这个问题是当这个代码被检入到仓库时,任何碰巧看的人都可以看到它。为了解决这个问题,我试过了:

// The URIs that should be excluded from CSRF verification
protected $except = [
    'UG2Yu8QzHY6KbxvLNxcRs0HVy9lQnKsx',
];

但PHP并不喜欢它。我也试过在这里使用路线名称:

protected $except = [
    config('auto-deploy.route'),
];

但这也不起作用。它必须是实际的URL。实际工作的事情是覆盖构造函数:

protected $except = [
    'autodeployroute',
];

但这必须是安装说明的一部分,并且对于Laravel软件包来说是一个不寻常的安装步骤。我觉得这是我最终采用的解决方案,因为我觉得要求用户这么做并不是那么困难。而且它至少可能使他们意识到他们即将安装的包装规避了一些Laravel内置的安全性。

解决方法尝试#2:protected $except = []; public function __construct(\Illuminate\Contracts\Encryption\Encrypter $encrypter) { parent::__construct($encrypter); $this->except[] = config('auto-deploy.route'); } catch

我接下来要做的就是看看我是否可以抓住异常,然后忽略它并继续前进,即:

TokenMismatchException
是的,现在就来嘲笑我吧。愚蠢的wabbit,这不是public function handle($request, Closure $next) { if ($request->secure() && $request->path() === config('auto-deploy.route')) { if ($request->secure()) { // handle authenticating webhook request if (/* webhook request is authentic */) { // try to continue on to controller try { // this will eventually trigger the CSRF verification $response = $next($request); } catch (TokenMismatchException $e) { // but, maybe we can just ignore it and move on... return $response; } } else { // abort if not authenticated abort(403); } } else { // request NOT submitted via HTTPS abort(403); } } // Passthrough if it's not our secret route return $next($request); } 的工作方式!当然try/catch块中未定义$response。如果我尝试在catch块中执行$next($request),则只会再次与catch进行对比。

解决方法尝试#3:在中间件

中运行我的所有代码

当然,我可能忘记使用TokenMismatchException部署逻辑并触发中间件Controller方法中的所有内容。请求生命周期将在那里结束,我永远不会让其余的中间件传播。我无法感觉到有一些不优雅的东西,并且它偏离了Laravel构建的整体设计模式,以至于最终导致维护和协作难以向前发展。至少我知道它会起作用。

解决方法尝试#4:修改handle()

Philip Brown has an excellent tutorial describing the Pipeline pattern以及如何在Laravel中实现它。 Laravel的中间件使用这种模式。我想也许,也许,有一种方法可以访问Pipeline对象,该对象将中间件包排队,循环遍历它们,并为我的路由删除CSRF。我可以说,有很多方法可以添加新元素到管道,但无法找出其中的内容或以任何方式修改它。如果您知道某种方式,请告诉我!!!

解决方法尝试#5:使用Pipeline特征

我还没有彻底调查过这个问题但是,似乎这个特性最近被添加到允许测试路线而不必担心中间件。它显然不适合生产,并且禁用中间件意味着我必须提出一个全新的解决方案来确定如何让我的包完成它的工作。我觉得这不是可行的方法。

解决方法尝试#6:放弃。只需使用ForgeEnvoyer

即可

为什么重新发明轮子?为什么不直接支付已经支持推送部署的这些服务中的一个或两个,而不是为了解决我自己的软件包的问题呢?嗯,首先,我只为我的服务器每月支付5美元,所以不知何故,为这些服务之一每月另外支付5美元或10美元的经济效益并不合适。我是一名建立应用程序以支持我的教学的老师。它们都没有产生收入,虽然我可能负担得起,但随着时间的推移,这种事情会增加。

讨论

好的,所以我花了两天的时间来解决这个问题,这就是让我在这里寻求帮助的原因。你有解决方案吗?如果您已经读过这篇文章,也许您会沉溺于几个结束的想法。

思想#1:向Laravel家伙致敬,认真对待安全!

我真的印象深刻的是编写一个绕过内置安全机制的软件包是多么困难。我不是在谈论"规避"在我尝试做某事的方法中,但是从某种意义上说,我试图写一个合法的软件包来节省我和很多其他人的时间,但是,效果,要求他们相信我"通过可能将其打开到恶意部署触发器来确保其应用程序的安全性。这应该很难做到,而且确实如此。

思想#2:也许我不应这样做

通常,如果某些内容很难或无法在代码中实现,那就是设计。也许我的Bad Design™想要自动化这个软件包的整个安装过程。也许这是告诉我的代码,"不要这样做!"你觉得怎么样?

总之,这里有两个问题:

  1. 你知道一种我没想到的方法吗?
  2. 这是一个糟糕的设计吗?我不应该这样做吗?
  3. 感谢您的阅读,并感谢您的深思熟虑的答案。

    P.S。在有人说出来之前,我知道this might be a duplicate,但我提供了比其他海报更多的细节,他也没有找到解决方案。

2 个答案:

答案 0 :(得分:2)

我知道在生产代码中使用Reflection API并不是一个好习惯,但这是我能想到的唯一无需额外配置的解决方案。这更像是一个概念验证,我不会在生产代码中使用它。

我认为更好,更稳定的解决方案是让用户更新他的中间件以使用您的软件包。

tl; dr - 你可以把它放在你的包启动代码中:

// Just remove CSRF middleware when we hit the deploy route
if(request()->is(config('auto-deploy.route')))
{
    // Create a reflection object of the app instance
    $appReflector = new ReflectionObject(app());

    // When dumping the App instance, it turns out that the
    // global middleware is registered at:
    // Application
    //  -> instances
    //   -> Illuminate\Contracts\Http\Kernel
    //    -> ... Somewhere in the 'middleware' array
    //
    // The 'instance' property of the App object is not accessible
    // by default, so we have to make it accessible in order to
    // get and set its value.
    $instancesProperty = $appReflector->getProperty('instances');
    $instancesProperty->setAccessible(true);
    $instances = $instancesProperty->getValue(app());
    $kernel = $instances['Illuminate\Contracts\Http\Kernel'];

    // Now we got the Kernel instance.
    // Again, we have to set the accessibility of the instance.
    $kernelReflector = new ReflectionObject($kernel);
    $middlewareProperty = $kernelReflector->getProperty('middleware');
    $middlewareProperty->setAccessible(true);
    $middlewareArray = $middlewareProperty->getValue($kernel);

    // The $middlewareArray contains all global middleware.
    // We search for the CSRF entry and remove it if it exists.
    foreach ($middlewareArray as $i => $middleware)
    {
        if ($middleware == 'App\Http\Middleware\VerifyCsrfToken')
        {
            unset($middlewareArray[ $i ]);
            break;
        }
    }

    // The last thing we have to do is to update the altered
    // middleware array on the Kernel instance.
    $middlewareProperty->setValue($kernel, $middlewareArray);
}

答案 1 :(得分:1)

我没有用Laravel 5.1对它进行测试 - 对于5.2它可以工作。

因此,您可以创建一个Route::group,您可以明确说出要使用的中间件。

例如,在您的ServiceProvider中,您可以执行以下操作:

    \Route::group([
        'middleware' => ['only-middleware-you-need']
    ], function () {
        require __DIR__ . '/routes.php';
    });

因此,只需排除VerifyCsrfToken中间件,然后输入您需要的内容。