Laravel:防止用户修改其他用户的待办事项(Todo)
你遇到的问题核心在于:Auth::user()->todos 返回的是Eloquent集合(因为hasManyThrough关联的是多条记录),所以直接访问->id会报错,而取first()->id的逻辑完全不合理——它只检查了你第一个待办事项的清单ID和当前待办的清单ID是否一致,根本无法确保当前待办事项属于你。
下面提供几种从简单到优雅的解决方案,覆盖不同场景的需求:
方案1:直接通过关联关系验证(快速实现)
既然Todo属于Todolist,而Todolist属于User,我们可以直接验证当前待办事项的所属清单的用户ID是否等于当前登录用户ID:
public function edit(Todo $todo) { // 检查当前todo的todolist所属用户是否是当前登录用户 if ($todo->todolist->user_id === Auth::id()) { return view('todo/edit', ['todo' => $todo]); } abort(403, 'Unauthorized action.'); }
这个方案的优点是逻辑直接、代码简洁,不需要加载用户所有的待办事项,只需要额外查询一次todolist关联(Laravel的延迟加载会自动处理)。
方案2:使用关联查询验证(性能更优)
如果担心延迟加载的额外查询,可以用whereHas直接在数据库层面做验证,避免加载不必要的模型:
public function edit(Todo $todo) { $isAuthorized = Auth::user()->todos()->where('todos.id', $todo->id)->exists(); if ($isAuthorized) { return view('todo/edit', ['todo' => $todo]); } abort(403, 'Unauthorized action.'); }
这个方法直接在数据库中查询当前用户是否拥有该待办事项,返回的是布尔值,性能更优,尤其是用户待办事项较多的时候。
方案3:使用Laravel策略(推荐,适合中大型项目)
Laravel的**Policy(策略)**是处理模型权限的官方推荐方式,它能把权限逻辑从控制器中剥离出来,让代码更整洁、可复用。
步骤1:生成Todo策略
运行Artisan命令生成策略文件:
php artisan make:policy TodoPolicy --model=Todo
步骤2:编写策略逻辑
打开app/Policies/TodoPolicy.php,添加view、update、delete方法的权限验证逻辑:
namespace App\Policies; use App\Models\User; use App\Models\Todo; class TodoPolicy { /** * 验证用户是否能查看待办事项 */ public function view(User $user, Todo $todo): bool { return $user->id === $todo->todolist->user_id; } /** * 验证用户是否能更新待办事项 */ public function update(User $user, Todo $todo): bool { // 复用view的逻辑,因为编辑权限和查看权限通常一致 return $this->view($user, $todo); } /** * 验证用户是否能删除待办事项 */ public function delete(User $user, Todo $todo): bool { return $this->view($user, $todo); } }
步骤3:注册策略
打开app/Providers/AuthServiceProvider.php,在$policies数组中注册Todo策略:
protected $policies = [ \App\Models\Todo::class => \App\Policies\TodoPolicy::class, ];
步骤4:在控制器中使用策略
现在你的控制器方法可以简化成这样,Laravel会自动帮你处理权限验证:
public function edit(Todo $todo) { // 自动调用TodoPolicy的update方法验证权限,不通过则抛出403 $this->authorize('update', $todo); return view('todo/edit', ['todo' => $todo]); } // 同理,查看和删除方法也可以这样写: public function show(Todo $todo) { $this->authorize('view', $todo); return view('todo/show', ['todo' => $todo]); } public function destroy(Todo $todo) { $this->authorize('delete', $todo); $todo->delete(); return redirect()->route('todos.index'); }
方案4:全局路由模型绑定约束(最高级,自动验证)
如果希望在路由解析阶段就自动验证权限,不需要在每个控制器方法中写代码,可以使用路由模型绑定的全局约束:
打开app/Providers/RouteServiceProvider.php,在boot方法中添加:
public function boot() { parent::boot(); // 绑定todo路由参数时自动验证权限 Route::bind('todo', function ($value) { $todo = Todo::find($value); // 如果todo不存在,或者不属于当前用户,直接返回403 if (!$todo || $todo->todolist->user_id !== Auth::id()) { abort(403, 'Unauthorized action.'); } return $todo; }); }
这样,当用户访问类似/todos/1/edit的路由时,如果该todo不属于当前用户,会直接抛出403错误,你的控制器方法可以完全不需要权限判断:
public function edit(Todo $todo) { return view('todo/edit', ['todo' => $todo]); }
总结
- 小项目或者快速实现:用方案1或方案2
- 中大型项目,追求代码整洁和可维护性:用方案3(策略)
- 希望全局统一处理权限,减少控制器代码:用方案4(路由绑定约束)
内容的提问来源于stack exchange,提问作者wiwa1978

