Laravel嵌套路由权限控制

2026-09-09 31

项目管理系统经常使用 /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 $projectTask $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();

现在,请求需要同时满足两个条件:

  1. 任务属于地址中指定的项目。
  2. 当前用户具有项目查看权限。

如果任务还有独立的可见性规则,例如仅负责人可以查看,则需要再对任务执行相应的 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_idslug 的联合唯一索引。

启用 scopeBindings 后,还需要 Policy 吗?

需要。作用域绑定不能判断当前登录用户的身份和业务权限,也不能替代团队隔离、租户隔离等授权逻辑。

更新和删除任务也适用吗?

适用。更新和删除接口同样可以启用作用域绑定,但应分别执行 updatedelete 等授权检查,不能因为用户具有查看权限,就允许其修改或删除任务。

  • 广告合作

  • QQ群号:4114653

温馨提示:
1、本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com(#改为@)。 2、本站原创内容未经允许不得转裁,转载请注明出处“站长百科”和原文地址。