我想在我的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)表明你可以。如果是这样,这将是理想的,除了我不能看到(在模型中)如何理解保存已被取消并向客户端返回正确的错误消息?
答案 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');
}
}