授权
简介
除了提供内置的认证服务,Laravel 还提供了一种简单的方式来授权用户对给定资源的操作。例如,即使用户已经通过认证,他们也不一定有权限去更新或删除你应用管理的某些 Eloquent 模型或数据库记录。Laravel 的授权(Authorization)功能提供了一种简洁、有组织的方式来管理这类权限检查。
Laravel 提供了两种主要的操作授权方式:Gate 和策略(Policy)。可以把 Gate 和策略类比为路由和控制器。Gate 提供了一种简单的、基于闭包的授权方式,而策略就像控制器一样,把授权逻辑围绕某个特定模型或资源组织在一起。本文档会先介绍 Gate,再看策略。
你在构建应用时不需要在 Gate 和策略之间二选一。大多数应用会同时用到两者,这完全没问题!Gate 最适合那些与特定模型或资源无关的操作,比如查看管理后台仪表盘。而策略则适合在你需要对某个特定模型或资源进行授权时使用。
Gate
定义 Gate
注意
Gate 是学习 Laravel 授权基础的好方式;不过在构建正式的 Laravel 应用时,你应该考虑用策略来组织你的授权规则。
Gate 其实就是判断用户是否有权执行某个操作的闭包。通常在 App\Providers\AppServiceProvider 类的 boot() 方法中用 Gate 门面来定义。Gate 的第一个参数始终是一个用户实例,还可以接收其他参数,比如相关的 Eloquent 模型。
下面的例子定义了一个 Gate,用来判断某个用户能不能更新指定的 App\Models\Post 模型。这个 Gate 通过比较用户的 id 和创建文章的 user_id 来判断:
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}
跟控制器一样,Gate 也可以用类回调数组来定义:
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::define('update-post', [PostPolicy::class, 'update']);
}
通过 Gate 授权操作
要通过 Gate 授权某个操作,可以使用 Gate 门面提供的 allows() 或 denies() 方法。注意你不需要手动传入当前已认证用户——Laravel 会自动把用户传给 Gate 闭包。通常在控制器中执行需要授权的操作之前调用这些方法:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post): RedirectResponse
{
if (! Gate::allows('update-post', $post)) {
abort(403);
}
// Update the post...
return redirect('/posts');
}
}
如果你想判断的不是当前已认证用户,而是另一个用户是否有权执行某操作,可以用 Gate 门面的 forUser() 方法:
if (Gate::forUser($user)->allows('update-post', $post)) {
// 该用户可以更新文章...
}
if (Gate::forUser($user)->denies('update-post', $post)) {
// 该用户不能更新文章...
}
你也可以用 any() 或 none() 方法一次授权多个操作:
if (Gate::any(['update-post', 'delete-post'], $post)) {
// 用户可以更新或删除文章...
}
if (Gate::none(['update-post', 'delete-post'], $post)) {
// 用户既不能更新也不能删除文章...
}
授权或抛出异常
如果你希望尝试授权某个操作并在用户无权时自动抛出 Illuminate\Auth\Access\AuthorizationException,可以用 Gate 门面的 authorize() 方法。AuthorizationException 的实例会被 Laravel 自动转换为 403 HTTP 响应:
Gate::authorize('update-post', $post);
// 操作已授权...
传递额外上下文
用于授权能力的 Gate 方法(allows()、denies()、check()、any()、none()、authorize()、can()、cannot())以及授权 Blade 指令(@can、@cannot、@canany)都可以接收数组作为第二个参数。这些数组元素会作为参数传给 Gate 闭包,可用于授权决策时的额外上下文:
use App\Models\Category;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define('create-post', function (User $user, Category $category, bool $pinned) {
if (! $user->canPublishToGroup($category->group)) {
return false;
} elseif ($pinned && ! $user->canPinPosts()) {
return false;
}
return true;
});
if (Gate::check('create-post', [$category, $pinned])) {
// 用户可以创建文章...
}
Gate 响应
到目前为止,我们只看了返回简单布尔值的 Gate。但有时你可能需要返回更详细的响应,包括错误消息。这时可以从 Gate 中返回一个 Illuminate\Auth\Access\Response 实例:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::deny('You must be an administrator.');
});
即使你从 Gate 返回了授权响应,Gate::allows() 方法仍然返回简单布尔值;不过你可以用 Gate::inspect() 方法获取 Gate 返回的完整授权响应:
$response = Gate::inspect('edit-settings');
if ($response->allowed()) {
// 操作已授权...
} else {
echo $response->message();
}
当使用 Gate::authorize() 方法时(未授权会抛出 AuthorizationException),授权响应中的错误消息会传递到 HTTP 响应中:
Gate::authorize('edit-settings');
// 操作已授权...
自定义 HTTP 响应状态码
当操作通过 Gate 被拒绝时,默认返回 403 HTTP 响应;但有时返回其他 HTTP 状态码会更合适。你可以用 Illuminate\Auth\Access\Response 类的 denyWithStatus() 静态构造器来自定义授权检查失败时返回的 HTTP 状态码:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::denyWithStatus(404);
});
用 404 响应来隐藏资源是 Web 应用中很常见的模式,所以 Laravel 提供了 denyAsNotFound() 便捷方法:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::denyAsNotFound();
});
拦截 Gate 检查
有时你可能希望给某个特定用户授予所有权限。你可以用 before() 方法定义一个在所有其他授权检查之前运行的闭包:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::before(function (User $user, string $ability) {
if ($user->isAdministrator()) {
return true;
}
});
如果 before 闭包返回了非 null 的结果,该结果将作为授权检查的最终结果。
你也可以用 after() 方法定义一个在所有其他授权检查执行之后运行的闭包:
use App\Models\User;
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($user->isAdministrator()) {
return true;
}
});
after 闭包返回的值不会覆盖授权检查的结果,除非 Gate 或策略返回了 null。
内联授权
有时你想判断当前已认证用户是否有权执行某个操作,但又不想专门写一个 Gate。Laravel 允许你通过 Gate::allowIf() 和 Gate::denyIf() 方法执行这类「内联」授权检查。内联授权不会执行任何已定义的 "before" 或 "after" 授权钩子:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::allowIf(fn (User $user) => $user->isAdministrator());
Gate::denyIf(fn (User $user) => $user->banned());
如果操作未被授权或当前没有已认证用户,Laravel 会自动抛出 Illuminate\Auth\Access\AuthorizationException 异常。AuthorizationException 的实例会被 Laravel 的异常处理器自动转换为 403 HTTP 响应。
创建策略
生成策略
策略(Policy)是围绕特定模型或资源组织授权逻辑的类。例如,如果你的应用是一个博客,你可能有一个 App\Models\Post 模型和一个对应的 App\Policies\PostPolicy 来授权用户创建或更新文章等操作。
你可以用 make:policy Artisan 命令生成策略。生成的策略会放在 app/Policies 目录中。如果你的应用中没有这个目录,Laravel 会自动创建它:
php artisan make:policy PostPolicy
make:policy 命令会生成一个空的策略类。如果你想生成一个包含查看、创建、更新和删除资源的示例策略方法的类,可以在执行命令时加上 --model 选项:
php artisan make:policy PostPolicy --model=Post
注册策略
策略发现
默认情况下,只要模型和策略遵循 Laravel 的标准命名约定,Laravel 就能自动发现策略。具体来说,策略必须放在包含模型的目录或其上级的 Policies 目录中。举个例子,模型可以放在 app/Models 目录中,而策略放在 app/Policies 目录中。在这种情况下,Laravel 会先检查 app/Models/Policies,然后检查 app/Policies。此外,策略名称必须与模型名称匹配并加上 Policy 后缀。所以 User 模型对应的策略类是 UserPolicy。
如果你想自定义策略发现逻辑,可以用 Gate::guessPolicyNamesUsing() 方法注册一个自定义的策略发现回调。通常在 AppServiceProvider 的 boot() 方法中调用:
use Illuminate\Support\Facades\Gate;
Gate::guessPolicyNamesUsing(function (string $modelClass) {
// 返回给定模型对应的策略类名...
});
手动注册策略
使用 Gate 门面,你可以在 AppServiceProvider 的 boot() 方法中手动注册策略和对应的模型:
use App\Models\Order;
use App\Policies\OrderPolicy;
use Illuminate\Support\Facades\Gate;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::policy(Order::class, OrderPolicy::class);
}
或者,你也可以在模型类上放置 UsePolicy 属性来告知 Laravel 该模型对应的策略:
<?php
namespace App\Models;
use App\Policies\OrderPolicy;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
#[UsePolicy(OrderPolicy::class)]
class Order extends Model
{
//
}
编写策略
策略方法
策略类注册之后,你就可以为它要授权的每个操作添加方法了。例如,我们在 PostPolicy 上定义一个 update() 方法来判断给定的 App\Models\User 能否更新给定的 App\Models\Post 实例。
update() 方法接收一个 User 和一个 Post 实例作为参数,应该返回 true 或 false 来表示用户是否有权更新该 Post。在这个例子中,我们验证用户的 id 是否匹配文章的 user_id:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
你可以继续在策略上定义它需要授权的其他操作方法。例如,你可以定义 view() 或 delete() 方法来授权各种 Post 相关操作,但要记住你可以给策略方法取任何你喜欢的名字。
如果你在用 Artisan 命令生成策略时使用了 --model 选项,它已经包含了 viewAny、view、create、update、delete、restore 和 forceDelete 操作的方法。
信息
所有策略都通过 Laravel 的服务容器解析,这意味着你可以在策略构造函数中类型提示任何所需的依赖项,它们会被自动注入。
策略响应
到目前为止,我们只看了返回简单布尔值的策略方法。但有时你可能需要返回更详细的响应,包括错误消息。这时可以从策略方法中返回一个 Illuminate\Auth\Access\Response 实例:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('You do not own this post.');
}
当从策略返回授权响应时,Gate::allows() 方法仍然返回简单布尔值;不过你可以用 Gate::inspect() 方法获取 Gate 返回的完整授权响应:
use Illuminate\Support\Facades\Gate;
$response = Gate::inspect('update', $post);
if ($response->allowed()) {
// 操作已授权...
} else {
echo $response->message();
}
当使用 Gate::authorize() 方法时(未授权会抛出 AuthorizationException),授权响应中的错误消息会传递到 HTTP 响应中:
Gate::authorize('update', $post);
// 操作已授权...
自定义 HTTP 响应状态码
当操作通过策略方法被拒绝时,默认返回 403 HTTP 响应;但有时返回其他 HTTP 状态码会更合适。你可以用 Illuminate\Auth\Access\Response 类的 denyWithStatus() 静态构造器来自定义授权检查失败时返回的 HTTP 状态码:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyWithStatus(404);
}
用 404 响应来隐藏资源是 Web 应用中很常见的模式,所以 Laravel 提供了 denyAsNotFound() 便捷方法:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyAsNotFound();
}
不需要模型的方法
有些策略方法只接收当前已认证用户的实例。最常见的情况是授权 create 操作。例如,如果你在做一个博客,你可能想判断某个用户是否有权创建任何文章。在这种情况下,你的策略方法只需要接收用户实例:
/**
* Determine if the given user can create posts.
*/
public function create(User $user): bool
{
return $user->role == 'writer';
}
访客用户
默认情况下,如果传入的 HTTP 请求不是由已认证用户发起的,所有 Gate 和策略都会自动返回 false。不过,你可以通过声明「可选」类型提示或为用户参数提供 null 默认值,来让这些授权检查进入你的 Gate 和策略:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*/
public function update(?User $user, Post $post): bool
{
return $user?->id === $post->user_id;
}
}
策略过滤器
对于某些用户,你可能希望在给定策略中授权所有操作。为此,在策略上定义一个 before() 方法。before() 方法会在策略中的任何其他方法之前执行,让你有机会在实际的策略方法被调用之前就授权操作。这个功能最常用于授权应用管理员执行任何操作:
use App\Models\User;
/**
* Perform pre-authorization checks.
*/
public function before(User $user, string $ability): bool|null
{
if ($user->isAdministrator()) {
return true;
}
return null;
}
如果你想拒绝某类用户的所有授权检查,可以从 before() 方法返回 false。如果返回 null,授权检查会继续交给策略方法处理。
注意
如果策略类中不包含与被检查能力名称匹配的方法,before() 方法不会被调用。
使用策略授权操作
通过 User 模型
Laravel 应用自带的 App\Models\User 模型有两个用于授权操作的便捷方法:can() 和 cannot()。这两个方法接收你要授权的操作名称和相关模型。例如,判断用户是否有权更新给定的 App\Models\Post 模型。通常在控制器方法中调用:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post): RedirectResponse
{
if ($request->user()->cannot('update', $post)) {
abort(403);
}
// Update the post...
return redirect('/posts');
}
}
如果给定模型注册了策略,can() 方法会自动调用对应策略并返回布尔结果。如果没有注册策略,can() 方法会尝试调用与操作名匹配的闭包 Gate。
不需要模型的操作
记住,有些操作对应的策略方法(如 create)不需要模型实例。在这种情况下,你可以把类名传给 can() 方法。类名将用于决定使用哪个策略来授权操作:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* Create a post.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->cannot('create', Post::class)) {
abort(403);
}
// Create the post...
return redirect('/posts');
}
}
通过 Gate 门面
除了 App\Models\User 模型上的便捷方法,你始终可以通过 Gate 门面的 authorize() 方法来授权操作。
与 can() 方法类似,这个方法接收你要授权的操作名称和相关模型。如果操作未被授权,authorize() 方法会抛出 Illuminate\Auth\Access\AuthorizationException 异常,Laravel 异常处理器会自动将其转换为 403 状态码的 HTTP 响应:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
/**
* Update the given blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', $post);
// The current user can update the blog post...
return redirect('/posts');
}
}
不需要模型的操作
如前所述,有些策略方法如 create 不需要模型实例。在这种情况下,你应该把类名传给 authorize() 方法。类名将用于决定使用哪个策略来授权操作:
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
/**
* Create a new blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): RedirectResponse
{
Gate::authorize('create', Post::class);
// The current user can create blog posts...
return redirect('/posts');
}
通过中间件
Laravel 包含一个中间件,可以在请求到达你的路由或控制器之前就授权操作。默认情况下,Illuminate\Auth\Middleware\Authorize 中间件可以通过 can 中间件别名附加到路由上,这个别名由 Laravel 自动注册。来看一个用 can 中间件授权用户更新文章的例子:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新文章...
})->middleware('can:update,post');
在这个例子中,我们给 can 中间件传了两个参数。第一个是要授权的操作名称,第二个是要传给策略方法的路由参数。因为这里使用了隐式模型绑定,一个 App\Models\Post 模型会被传给策略方法。如果用户无权执行该操作,中间件会返回 403 状态码的 HTTP 响应。
为了方便,你也可以用路由的 can() 方法来附加 can 中间件:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新文章...
})->can('update', 'post');
如果你使用控制器中间件属性,可以通过 Authorize 属性来应用 can 中间件:
use Illuminate\Routing\Attributes\Controllers\Authorize;
#[Authorize('update', 'post')]
public function update(Post $post)
{
// 当前用户可以更新文章...
}
不需要模型的操作
同样,有些策略方法如 create 不需要模型实例。在这种情况下,你可以把类名传给中间件。类名将用于决定使用哪个策略来授权操作:
Route::post('/post', function () {
// 当前用户可以创建文章...
})->middleware('can:create,App\Models\Post');
在字符串中间件定义里指定完整类名可能很长。所以你也可以用路由的 can() 方法来附加 can 中间件:
use App\Models\Post;
Route::post('/post', function () {
// 当前用户可以创建文章...
})->can('create', Post::class);
通过 Blade 模板
编写 Blade 模板时,你可能希望只在用户有权执行某操作时才显示页面的某个部分。例如,你可能只想在用户有权更新文章时才显示更新表单。在这种情况下,可以使用 @can 和 @cannot 指令:
@can('update', $post)
<!-- 当前用户可以更新文章... -->
@elsecan('create', App\Models\Post::class)
<!-- 当前用户可以创建新文章... -->
@else
<!-- ... -->
@endcan
@cannot('update', $post)
<!-- 当前用户不能更新文章... -->
@elsecannot('create', App\Models\Post::class)
<!-- 当前用户不能创建新文章... -->
@endcannot
这些指令是 @if 和 @unless 语句的便捷写法。上面的 @can 和 @cannot 语句等价于:
@if (Auth::user()->can('update', $post))
<!-- 当前用户可以更新文章... -->
@endif
@unless (Auth::user()->can('update', $post))
<!-- 当前用户不能更新文章... -->
@endunless
你也可以判断用户是否有权执行给定数组中的任何一个操作。用 @canany 指令:
@canany(['update', 'view', 'delete'], $post)
<!-- 当前用户可以更新、查看或删除文章... -->
@elsecanany(['create'], \App\Models\Post::class)
<!-- 当前用户可以创建文章... -->
@endcanany
不需要模型的操作
和大多数其他授权方法一样,如果操作不需要模型实例,你可以把类名传给 @can 和 @cannot 指令:
@can('create', App\Models\Post::class)
<!-- 当前用户可以创建文章... -->
@endcan
@cannot('create', App\Models\Post::class)
<!-- 当前用户不能创建文章... -->
@endcannot
传递额外上下文
使用策略授权操作时,你可以把数组作为第二个参数传给各种授权函数和辅助方法。数组中的第一个元素用于决定调用哪个策略,其余元素作为参数传给策略方法,可用于授权决策时的额外上下文。例如,考虑以下包含额外 $category 参数的 PostPolicy 方法定义:
/**
* Determine if the given post can be updated by the user.
*/
public function update(User $user, Post $post, int $category): bool
{
return $user->id === $post->user_id &&
$user->canUpdateCategory($category);
}
当尝试判断已认证用户能否更新给定文章时,可以这样调用策略方法:
/**
* Update the given blog post.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', [$post, $request->category]);
// The current user can update the blog post...
return redirect('/posts');
}
授权与 Inertia
虽然授权必须在服务端处理,但为前端应用提供授权数据来正确渲染 UI 通常很方便。Laravel 没有定义将授权信息暴露给 Inertia 前端的固定约定。
不过,如果你使用了 Laravel 基于 Inertia 的起步套件,你的应用已经包含了一个 HandleInertiaRequests 中间件。在这个中间件的 share() 方法中,你可以返回共享数据,这些数据会提供给应用中所有的 Inertia 页面。这个共享数据就是定义用户授权信息的便捷位置:
<?php
namespace App\Http\Middleware;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
// ...
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request)
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
'permissions' => [
'post' => [
'create' => $request->user()->can('create', Post::class),
],
],
],
];
}
}