本节摘要:授权回答"你能干什么"。Gate 适合登记零散的操作权限,Policy 把"围绕一种资源的全部权限"收进一个类;两者在控制器里都收口到 authorize 一行调用。API 场景则换一套通行逻辑:Sanctum 发令牌、带令牌、验令牌。本节把三套工具接进工单系统,完成第三章遗留的归属过滤的"正式版"。
上一节认了人,但"谁能改单、谁能删单"的判断还散落在控制器里。散落式授权的下场:加一个"只读访客"角色要全文搜索 if;两个接口的判断标准悄悄不一致,越权就从缝隙里漏出去。授权要收口:规则一处声明,检查一行调用。Laravel 给了两个容器——Gate 与 Policy——再加一套 API 场景的令牌方案 Sanctum。
Gate 适合与具体资源无关的能力判断:
<?php // AppServiceProvider 的 boot 里登记(Laravel 11 也可用 Gate::before 做全局放行) use Illuminate\Support\Facades\Gate; public function boot(): void { // 管理员全局放行:所有 Gate 与 Policy 检查先过这一关 Gate::before(fn ($user) => $user->is_admin ? true : null); // 登记一条能力:谁可以进后台 Gate::define('access-dashboard', fn ($user) => $user->is_admin); } // 使用侧:控制器、视图、路由随处可查 if (Gate::allows('access-dashboard')) { /* ... */ } $this->authorize('access-dashboard'); // 失败自动 403
判断一多、资源一明确(工单能不能看、改、删、结),就该上 Policy。它把一种资源的权限收进一个类,方法名即动作名:
php artisan make:policy OrderPolicy --model=Order
<?php namespace App\Policies; use App\Models\Order; use App\Models\User; class OrderPolicy { // 通用规则:同队组的人才能碰这张单 public function view(User $user, Order $order): bool { return $user->team_id === $order->team_id; } public function update(User $user, Order $order): bool { return $user->team_id === $order->team_id && $order->status !== 'done'; } public function delete(User $user, Order $order): bool { return $user->is_admin || $user->id === $order->creator_id; } }
<?php // 控制器侧:一行调用,自动找对 Policy 与方法 public function update(UpdateOrderRequest $request, Order $order) { $this->authorize('update', $order); // 不过则 403,API 场景 JSON 403 $order->update($request->validated()); return back()->with('status', '工单已更新'); } // 表单验证里的授权检查也可以挪进来(呼应 4.2 的 authorize 方法) public function authorize(): bool { return $this->user()->can('create', Order::class); }
对照第三章:当时我们在路由绑定闭包里手写了 team_id 过滤,那是"查不到就 404"的隐式防越权;Policy 是显式的、可复用、可测试的正式方案。两者不冲突——绑定层保证查不出别人家的单,Policy 保证查得到的单也不是人人能动。角色权限的管理(多角色、权限包)成熟方案是 Spatie 的 permission 包,思路仍是"声明集中、检查一行",用到再引入。

移动端、小程序、第三方脚本没有浏览器 Cookie,session 方案接不住,通行方式换成令牌:
<?php use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; // 发证:登录接口验密后签发令牌 public function apiLogin(Request $request) { $request->validate([ 'email' => ['required', 'email'], 'password' => ['required'], 'device' => ['required', 'string'], ]); $user = \App\Models\User::where('email', $request->email)->first(); if (! $user || ! Hash::check($request->password, $user->password)) { throw ValidationException::withMessages(['email' => '凭据不正确']); } // 签发:令牌本身可带能力清单,abilities 在 middleware 里校验 $token = $user->createToken($request->device, ['orders:read', 'orders:write']); return response()->json(['token' => $token->plainTextToken]); }
<?php // 使用侧:路由挂 auth:sanctum 闸机,请求头带 Authorization: Bearer 令牌 Route::middleware('auth:sanctum')->group(function () { Route::get('/api/orders', [OrderApiController::class, 'index']); Route::post('/api/orders', [OrderApiController::class, 'store']) ->middleware('ability:orders:write'); });
Sanctum 的实用主义值得一夸:它不发明新协议,数据库里一张 personal_access_tokens 表存哈希后的令牌,验签就是查表比对;令牌按设备签发,用户在设置页能逐个吊销。选择上的分界线:浏览器内的用户会话用 session(配合 Cookie 加 CSRF),跨设备的程序调用用 Sanctum,需要 OAuth 级别的第三方授权委托才考虑 Passport。
⚠️ 常见坑:API 令牌等同密码,必须全程 HTTPS 传输;plainTextToken 只在签发那一刻可见,落库前就要交付给客户端;能力清单宁窄勿宽,只读客户端别顺手发写权限。
门禁装好了,项目正式投入运营。下一章走进幕后车间:队列把慢活挪到后台,调度器按时巡检,事件与通知负责全工地广播。