在Laravel中检查If-Match标头etag与模型

时间:2018-02-01 17:24:26

标签: laravel concurrency etag

我想在我的API中执行PUT / PATCH时实现并发安全检查。我可以在服务器响应中放置一个etag,表示给定资源的哈希值。我正在努力理解的是,在Laravel架构中,我可以验证客户端提供的哈希与要更新/替换的模型。

我考虑过路由中间件,但此时模型尚未解决,因此我无法获取模型哈希。如果我确实在中间件中解析了模型,那么将会有性能开销,因为路由将解析模型(api的工作量增加一倍)。至少我认为模型没有解决,是否有办法获得已解析的模型并传递到路线上,以便它不需要再次解决它?

我还考虑了应用于路由的策略,这最初看起来很完美,因为它接收到用户打算放置/补丁的已解析模型,但我只能在此处返回true / false。因此响应将是403禁止(如果在更新/替换请求之前资源状态已经改变)并且我看不到用412 Precondition Failed响应覆盖它。我可以在Handler.php中覆盖异常,但是任何“真正的”403响应也会被覆盖。

我不想要做的是在控制器方法中实现这一点,这似乎是一个强力解决方案。任何人都可以提出一个很好的通用“Laravel方式”解决方案吗?

非常感谢提前!

更新:看一下模型事件,看起来你可以通过在监听器处理程序中返回false来停止事件的传播,尽管我认为这不会阻止导致事件干扰的基础操作。有人知道情况确实如此吗?

我也看过模型观察者,虽然文档中没有关于取消事件的内容,但本文(https://laravel-tricks.com/tricks/cancelling-a-model-save-update-delete-through-events)表明你可以。如果是这样,这将是理想的,除了我不能看到(在模型中)如何理解保存已被取消并向客户端返回正确的错误消息?

1 个答案:

答案 0 :(得分:1)

模型事件可能就是它所做的事情。

https://laravel.com/docs/5.5/eloquent#events

public static function boot()
{
    parent::boot();

    /**
     * Check model hash vs header etag
     */
    static::updating(function (Model $model) {
         $etag = request()->header('etag');
         // check $etag against model hash, return false to prevent update
    });
}

您可以使用BaseModel和常见的哈希检查方法以这种方式制作一个很好的通用解决方案。

<强>更新

正如我理解事件传播一样,一旦你从听众那里返回了错误,那就是它的生命周期结束。后续听众将不会收到该活动。

  

有时,您可能希望停止将事件传播给其他人   听众。您可以通过从听众那里返回虚假来做到这一点   处理方法。

您使用什么传输方法(如果有的话)将消息返回给用户? WebSocket通过Echo? Echo将是一种简单的方法,通过Laravel Echo Server向私人频道上的经过身份验证的用户广播通知回复是非常简单的(ish)。

我通过从模型事件闭包中调回来接近它:

static::updating(function (Model $model) {
     $etag = request()->header('etag');
     // check $etag against model hash, return false to prevent update
     if ($failedCheck) dispatch(NotifyUserOfUpdateFailure::class, $model);
     else doUpdate()
});

修改

我认为这应该只在一个中间件中实现。因为我们可以访问route()帮助器,所以我们可以提取请求参数并在中间件中查询模型。

namespace VdPoel\src\Http\Middleware;

// say we have a Vehicle resource, with a base endpoint /api/vehicles.
//
// PUT or PATCH
// consider this url path is a valid api endpoint
// /api/vehicles/1
// defined some routes for 'vehicle' records using resource routes
// Route::resource('vehicles', 'VehicleController');
// or standard route definitions
// Route::put('/vehicles/{vehicle}', 'VehicleController@update')->middleware(CheckHash::class);
// Route::patch('/vehicles/{vehicle}', 'VehicleController@update')->middleware(CheckHash::class);

class CheckHash
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return \Illuminate\Http\Response|null
     */
    public function handle($request, $next)
    {
        if (!in_array($request->method(), ['PUT', 'PATCH'])) {
            return $next($request);
        }

        $hash = $request->header('e-tag');
        $vehicleId = $request->route()->parameter('vehicle');

        if ($vehicle = Vehicle::find($vehicleId)) {
            return $vehicle->getETagHash() === $hash ? $next($request) : abort(412, 'Hash does not match');
        }

        abort(404, 'Vehicle not found');
    }
}