项目管理系统经常使用 /projects/10/tasks/25 这样的地址查看任务。地址中同时包含项目 ID 和任务 ID,但这并不意味着 Laravel 会自动确认任务 25 属于项目 10。如果只按任务 ID 查询数据库,用户修改地址后,就可能访问到其他项目的任务。
Laravel 的作用域路由绑定可以将子资源查询限制在父资源范围内,再结合 Policy 检查用户权限,适合项目任务、店铺商品、团队文档等存在归属关系的业务。
以下示例采用 Laravel 11、12 常见的项目结构,假设项目表包含 user_id 字段,任务表包含 project_id 字段,且用户已经通过系统的身份验证。
一、为什么嵌套路由还需要检查资源归属
先看一条普通路由:
use App\Models\Project;
use App\Models\Task;
use Illuminate\Support\Facades\Route;
Route::get('/projects/{project}/tasks/{task}', function (
Project $project,
Task $task
) {
return response()->json($task);
})->middleware('auth');
这里使用了隐式模型绑定,Laravel 可以根据路由参数查找项目和任务。
但是,仅仅将两个参数写在同一个地址中,不能保证任务查询一定受项目限制。业务需要分别确认:
- 当前任务是否属于地址中的项目。
- 当前用户是否有权访问该项目及其任务。
scopeBindings() 解决资源归属问题,Policy 负责用户权限判断。
二、定义项目与任务的关联关系
作用域绑定需要通过父模型的关联关系查找子模型。先在 Project 模型中定义 tasks():
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Project extends Model
{
public function tasks(): HasMany
{
return $this->hasMany(Task::class);
}
}
然后在 Task 模型中定义所属项目:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model
{
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
}
在这个示例中,数据关系为:
projects.id → tasks.project_id
路由参数使用 {task} 时,Laravel 通常会按参数名称的复数形式寻找父模型上的 tasks() 关联。因此,路由参数和关联方法的命名需要匹配。
三、使用 scopeBindings 限制任务查询范围
为路由增加 scopeBindings():
use App\Models\Project;
use App\Models\Task;
use Illuminate\Support\Facades\Route;
Route::get('/projects/{project}/tasks/{task}', function (
Project $project,
Task $task
) {
return response()->json([
'project_id' => $project->id,
'task_id' => $task->id,
]);
})
->middleware('auth')
->scopeBindings();
启用后,Laravel 会通过当前项目的 tasks() 关联查询任务。
例如:
项目 10 包含任务 25
项目 20 包含任务 36
访问 /projects/10/tasks/25 时,可以找到对应任务。
访问 /projects/10/tasks/36 时,即使任务 36 在数据库中真实存在,因为它不属于项目 10,绑定也会失败,通常返回 HTTP 404。
如果一组路由都需要限制子资源范围,可以统一配置:
Route::middleware('auth')
->scopeBindings()
->group(function () {
Route::get(
'/projects/{project}/tasks/{task}',
[TaskController::class, 'show']
);
});
使用控制器写法时,需要导入实际的 TaskController,并在方法中保留 Project $project 和 Task $task 参数。
四、结合 Policy 检查用户访问权限
作用域绑定确认了任务所属的项目,但用户仍然可能同时修改项目 ID 和任务 ID,访问另一个项目中真实存在的任务。
因此,还需要增加用户权限检查。
创建项目策略:
php artisan make:policy ProjectPolicy --model=Project
在 app/Policies/ProjectPolicy.php 中编写:
namespace App\Policies;
use App\Models\Project;
use App\Models\User;
class ProjectPolicy
{
public function view(User $user, Project $project): bool
{
return (string) $user->id === (string) $project->user_id;
}
}
这里采用简单的“项目所有者可以查看”规则。团队协作系统应按实际成员关系、角色和权限调整,不能直接套用所有者判断。
为明确策略对应关系,可以在 AppServiceProvider 的现有 boot() 方法中注册:
use App\Models\Project;
use App\Policies\ProjectPolicy;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::policy(Project::class, ProjectPolicy::class);
}
然后将路由更新为:
use App\Models\Project;
use App\Models\Task;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
Route::get('/projects/{project}/tasks/{task}', function (
Project $project,
Task $task
) {
Gate::authorize('view', $project);
return response()->json([
'project_id' => $project->id,
'task_id' => $task->id,
]);
})
->middleware('auth')
->scopeBindings();
现在,请求需要同时满足两个条件:
- 任务属于地址中指定的项目。
- 当前用户具有项目查看权限。
如果任务还有独立的可见性规则,例如仅负责人可以查看,则需要再对任务执行相应的 Policy 检查。
五、验证正常访问与越权访问
可以准备两个用户、两个项目和各自的任务,按下面的情况测试:
| 测试情况 | 预期结果 |
|---|---|
| 项目所有者访问本项目任务 | HTTP 200 |
| 项目所有者在本项目地址中填写其他项目的任务 ID | HTTP 404 |
| 用户访问他人项目及其真实所属任务 | HTTP 403 |
| 项目或任务不存在 | HTTP 404 |
未登录时的响应取决于认证方式。普通网页路由可能跳转到登录页,请求 JSON 的接口通常返回 HTTP 401。
调试时,应分别检查作用域绑定和 Policy。前者解决项目与任务的归属,后者决定当前用户能否执行操作。
六、常见问题
使用 slug 作为任务地址,还能限制项目范围吗?
可以。例如:
Route::get('/projects/{project}/tasks/{task:slug}', function (
Project $project,
Task $task
) {
Gate::authorize('view', $project);
return response()->json([
'project_id' => $project->id,
'task_id' => $task->id,
]);
})
->middleware('auth')
->scopeBindings();
此时需要确保任务表存在 slug 字段,并合理约束其唯一性。如果允许不同项目使用相同的任务 slug,可以考虑建立 project_id 与 slug 的联合唯一索引。
启用 scopeBindings 后,还需要 Policy 吗?
需要。作用域绑定不能判断当前登录用户的身份和业务权限,也不能替代团队隔离、租户隔离等授权逻辑。
更新和删除任务也适用吗?
适用。更新和删除接口同样可以启用作用域绑定,但应分别执行 update、delete 等授权检查,不能因为用户具有查看权限,就允许其修改或删除任务。

