授权
简介
除了提供内置的 身份验证 服务外,Laravel 还提供了一种简单的方法来针对给定资源授权用户操作。例如,即使一个用户已通过身份验证,他们也可能无权更新或删除由您的应用程序管理的某些 Eloquent 模型或数据库记录。Laravel 的授权功能提供了一种简单、有组织的方式来管理这些类型的授权检查。
Laravel 提供了两种主要的授权动作方式:门面(Gates) 和 策略(Policies)。可以将门面和策略想象成路由和控制器。门面提供了一种简单的、基于闭包的授权方法,而策略则像控制器一样,围绕特定的模型或资源对逻辑进行分组。在本文档中,我们将先探索门面,然后再研究策略。
在构建应用程序时,您无需在仅使用门面或仅使用策略之间做出选择。大多数应用程序很可能包含门面和策略的混合,这完全没问题!门面最适用于与任何模型或资源无关的动作,例如查看管理员仪表板。相比之下,当您希望授权特定模型或资源的动作时,应使用策略。
门面(Gates)
编写门面
门面是学习 Laravel 授权功能基础知识的好方法;但是,在构建健壮的 Laravel 应用程序时,您应该考虑使用 策略 来组织您的授权规则。
门面只是决定用户是否有权执行给定动作的闭包。通常,门面是在 App\Providers\AppServiceProvider 类的 boot 方法中使用 Gate 门面定义的。门面总是接收一个用户实例作为其第一个参数,并且可以选择接收其他参数,例如相关的 Eloquent 模型。
在此示例中,我们将定义一个门面来确定用户是否可以更新给定的 App\Models\Post 模型。门面将通过将用户的 id 与创建该帖子的用户的 user_id 进行比较来实现这一点
1use App\Models\Post; 2use App\Models\User; 3use Illuminate\Support\Facades\Gate; 4 5/** 6 * Bootstrap any application services. 7 */ 8public function boot(): void 9{10 Gate::define('update-post', function (User $user, Post $post) {11 return $user->id === $post->user_id;12 });13}
像控制器一样,门面也可以使用类回调数组来定义
1use App\Policies\PostPolicy; 2use Illuminate\Support\Facades\Gate; 3 4/** 5 * Bootstrap any application services. 6 */ 7public function boot(): void 8{ 9 Gate::define('update-post', [PostPolicy::class, 'update']);10}
授权动作
要使用门面授权动作,您应该使用 Gate 门面提供的 allows 或 denies 方法。请注意,您不需要将当前经过身份验证的用户传递给这些方法。Laravel 会自动处理将用户传递到门面闭包中。通常在执行需要授权的动作之前,在应用程序的控制器中调用门面授权方法
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Post; 6use Illuminate\Http\RedirectResponse; 7use Illuminate\Http\Request; 8use Illuminate\Support\Facades\Gate; 9 10class PostController extends Controller11{12 /**13 * Update the given post.14 */15 public function update(Request $request, Post $post): RedirectResponse16 {17 if (! Gate::allows('update-post', $post)) {18 abort(403);19 }20 21 // Update the post...22 23 return redirect('/posts');24 }25}
如果您想确定除当前经过身份验证的用户之外的其他用户是否有权执行动作,可以使用 Gate 门面上的 forUser 方法
1if (Gate::forUser($user)->allows('update-post', $post)) {2 // The user can update the post...3}4 5if (Gate::forUser($user)->denies('update-post', $post)) {6 // The user can't update the post...7}
您可以使用 any 或 none 方法一次授权多个动作
1if (Gate::any(['update-post', 'delete-post'], $post)) {2 // The user can update or delete the post...3}4 5if (Gate::none(['update-post', 'delete-post'], $post)) {6 // The user can't update or delete the post...7}
授权或抛出异常
如果您想尝试授权一个动作,并且在用户不允许执行给定动作时自动抛出 Illuminate\Auth\Access\AuthorizationException,可以使用 Gate 门面的 authorize 方法。Laravel 会自动将 AuthorizationException 实例转换为 403 HTTP 响应
1Gate::authorize('update-post', $post);2 3// The action is authorized...
提供额外上下文
用于授权能力的门面方法(allows、denies、check、any、none、authorize、can、cannot)和授权 Blade 指令(@can、@cannot、@canany)可以将数组作为其第二个参数。这些数组元素作为参数传递给门面闭包,并可在进行授权决策时用于提供额外的上下文
1use App\Models\Category; 2use App\Models\User; 3use Illuminate\Support\Facades\Gate; 4 5Gate::define('create-post', function (User $user, Category $category, bool $pinned) { 6 if (! $user->canPublishToGroup($category->group)) { 7 return false; 8 } elseif ($pinned && ! $user->canPinPosts()) { 9 return false;10 }11 12 return true;13});14 15if (Gate::check('create-post', [$category, $pinned])) {16 // The user can create the post...17}
门面响应
到目前为止,我们只检查了返回简单布尔值的门面。但是,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从门面返回一个 Illuminate\Auth\Access\Response
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;4 5Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::deny('You must be an administrator.');9});
即使您从门面返回了授权响应,Gate::allows 方法仍然会返回一个简单的布尔值;但是,您可以使用 Gate::inspect 方法来获取门面返回的完整授权响应
1$response = Gate::inspect('edit-settings');2 3if ($response->allowed()) {4 // The action is authorized...5} else {6 echo $response->message();7}
当使用 Gate::authorize 方法(如果动作未被授权则抛出 AuthorizationException)时,授权响应提供的错误消息将传播到 HTTP 响应中
1Gate::authorize('edit-settings');2 3// The action is authorized...
自定义 HTTP 响应状态
当通过门面拒绝动作时,会返回 403 HTTP 响应;然而,有时返回替代的 HTTP 状态代码会很有用。您可以使用 Illuminate\Auth\Access\Response 类上的 denyWithStatus 静态构造函数自定义授权检查失败时返回的 HTTP 状态代码
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;4 5Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::denyWithStatus(404);9});
因为通过 404 响应隐藏资源是 Web 应用程序的一种常见模式,所以为了方便起见,提供了 denyAsNotFound 方法
1use App\Models\User;2use Illuminate\Auth\Access\Response;3use Illuminate\Support\Facades\Gate;4 5Gate::define('edit-settings', function (User $user) {6 return $user->isAdmin7 ? Response::allow()8 : Response::denyAsNotFound();9});
拦截门面检查
有时,您可能希望授予特定用户所有能力。您可以使用 before 方法定义一个在所有其他授权检查之前运行的闭包
1use App\Models\User;2use Illuminate\Support\Facades\Gate;3 4Gate::before(function (User $user, string $ability) {5 if ($user->isAdministrator()) {6 return true;7 }8});
如果 before 闭包返回一个非 null 的结果,则该结果将被视为授权检查的结果。
您可以使用 after 方法定义一个在所有其他授权检查之后执行的闭包
1use App\Models\User;2 3Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {4 if ($user->isAdministrator()) {5 return true;6 }7});
除非门面或策略返回了 null,否则 after 闭包返回的值不会覆盖授权检查的结果。
内联授权
有时,您可能希望在不编写与动作对应的专用门面的情况下,确定当前经过身份验证的用户是否有权执行给定动作。Laravel 允许您通过 Gate::allowIf 和 Gate::denyIf 方法执行这些类型的“内联”授权检查。内联授权不会执行任何已定义的 “之前”或“之后”授权钩子
1use App\Models\User;2use Illuminate\Support\Facades\Gate;3 4Gate::allowIf(fn (User $user) => $user->isAdministrator());5 6Gate::denyIf(fn (User $user) => $user->banned());
如果动作未被授权,或者当前没有任何用户经过身份验证,Laravel 会自动抛出 Illuminate\Auth\Access\AuthorizationException 异常。Laravel 的异常处理程序会自动将 AuthorizationException 实例转换为 403 HTTP 响应。
创建策略(Policies)
生成策略
策略是围绕特定模型或资源组织授权逻辑的类。例如,如果您的应用程序是一个博客,您可能有一个 App\Models\Post 模型和一个对应的 App\Policies\PostPolicy,用于授权创建或更新帖子等用户动作。
您可以使用 make:policy Artisan 命令生成策略。生成的策略将放置在 app/Policies 目录中。如果您的应用程序中不存在此目录,Laravel 将为您创建它
1php artisan make:policy PostPolicy
make:policy 命令将生成一个空的策略类。如果您想生成一个包含与查看、创建、更新和删除资源相关的示例策略方法的类,可以在执行命令时提供 --model 选项
1php 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 方法中调用
1use Illuminate\Support\Facades\Gate;2 3Gate::guessPolicyNamesUsing(function (string $modelClass) {4 // Return the name of the policy class for the given model...5});
手动注册策略
使用 Gate 门面,您可以在应用程序的 AppServiceProvider 的 boot 方法中手动注册策略及其对应的模型
1use App\Models\Order; 2use App\Policies\OrderPolicy; 3use Illuminate\Support\Facades\Gate; 4 5/** 6 * Bootstrap any application services. 7 */ 8public function boot(): void 9{10 Gate::policy(Order::class, OrderPolicy::class);11}
或者,您可以在模型类上放置 UsePolicy 属性,以通知 Laravel 该模型对应的策略
1<?php 2 3namespace App\Models; 4 5use App\Policies\OrderPolicy; 6use Illuminate\Database\Eloquent\Attributes\UsePolicy; 7use Illuminate\Database\Eloquent\Model; 8 9#[UsePolicy(OrderPolicy::class)]10class Order extends Model11{12 //13}
编写策略
策略方法
一旦注册了策略类,您就可以为它授权的每个动作添加方法。例如,让我们在 PostPolicy 上定义一个 update 方法,该方法确定给定的 App\Models\User 是否可以更新给定的 App\Models\Post 实例。
update 方法将接收一个 User 和一个 Post 实例作为其参数,并应返回 true 或 false,指示用户是否有权更新给定的 Post。因此,在此示例中,我们将验证用户的 id 是否与帖子上的 user_id 匹配
1<?php 2 3namespace App\Policies; 4 5use App\Models\Post; 6use App\Models\User; 7 8class PostPolicy 9{10 /**11 * Determine if the given post can be updated by the user.12 */13 public function update(User $user, Post $post): bool14 {15 return $user->id === $post->user_id;16 }17}
您可以根据需要继续在策略上定义其他方法,以用于它授权的各种动作。例如,您可以定义 view 或 delete 方法来授权各种与 Post 相关的动作,但请记住,您可以随意为策略方法命名。
如果您在通过 Artisan 控制台生成策略时使用了 --model 选项,它将已经包含用于 viewAny、view、create、update、delete、restore 和 forceDelete 动作的方法。
所有策略都通过 Laravel 服务容器 解析,允许您在策略的构造函数中对任何所需的依赖项进行类型提示,从而使它们被自动注入。
策略响应
到目前为止,我们只检查了返回简单布尔值的策略方法。但是,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从策略方法返回一个 Illuminate\Auth\Access\Response 实例
1use App\Models\Post; 2use App\Models\User; 3use Illuminate\Auth\Access\Response; 4 5/** 6 * Determine if the given post can be updated by the user. 7 */ 8public function update(User $user, Post $post): Response 9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::deny('You do not own this post.');13}
当从策略返回授权响应时,Gate::allows 方法仍然会返回一个简单的布尔值;但是,您可以使用 Gate::inspect 方法来获取门面返回的完整授权响应
1use Illuminate\Support\Facades\Gate;2 3$response = Gate::inspect('update', $post);4 5if ($response->allowed()) {6 // The action is authorized...7} else {8 echo $response->message();9}
当使用 Gate::authorize 方法(如果动作未被授权则抛出 AuthorizationException)时,授权响应提供的错误消息将传播到 HTTP 响应中
1Gate::authorize('update', $post);2 3// The action is authorized...
自定义 HTTP 响应状态
当通过策略方法拒绝动作时,会返回 403 HTTP 响应;然而,有时返回替代的 HTTP 状态代码会很有用。您可以使用 Illuminate\Auth\Access\Response 类上的 denyWithStatus 静态构造函数自定义授权检查失败时返回的 HTTP 状态代码
1use App\Models\Post; 2use App\Models\User; 3use Illuminate\Auth\Access\Response; 4 5/** 6 * Determine if the given post can be updated by the user. 7 */ 8public function update(User $user, Post $post): Response 9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::denyWithStatus(404);13}
因为通过 404 响应隐藏资源是 Web 应用程序的一种常见模式,所以为了方便起见,提供了 denyAsNotFound 方法
1use App\Models\Post; 2use App\Models\User; 3use Illuminate\Auth\Access\Response; 4 5/** 6 * Determine if the given post can be updated by the user. 7 */ 8public function update(User $user, Post $post): Response 9{10 return $user->id === $post->user_id11 ? Response::allow()12 : Response::denyAsNotFound();13}
无需模型的方法
有些策略方法只接收当前经过身份验证的用户实例。这种情况在授权 create 动作时最常见。例如,如果您正在创建一个博客,您可能希望确定用户是否有权创建任何帖子。在这些情况下,您的策略方法应该只期望接收一个用户实例
1/**2 * Determine if the given user can create posts.3 */4public function create(User $user): bool5{6 return $user->role == 'writer';7}
访客用户
默认情况下,如果传入的 HTTP 请求不是由经过身份验证的用户发起的,所有门面和策略都会自动返回 false。但是,您可以通过声明“可选”类型提示或为用户参数定义提供 null 默认值,允许这些授权检查传递到您的门面和策略中
1<?php 2 3namespace App\Policies; 4 5use App\Models\Post; 6use App\Models\User; 7 8class PostPolicy 9{10 /**11 * Determine if the given post can be updated by the user.12 */13 public function update(?User $user, Post $post): bool14 {15 return $user?->id === $post->user_id;16 }17}
策略过滤器
对于某些用户,您可能希望授权给定策略内的所有动作。要实现这一点,请在策略上定义一个 before 方法。before 方法将在策略上的任何其他方法之前执行,让您有机会在实际调用预期的策略方法之前授权该动作。此功能最常用于授权应用程序管理员执行任何动作
1use App\Models\User; 2 3/** 4 * Perform pre-authorization checks. 5 */ 6public function before(User $user, string $ability): bool|null 7{ 8 if ($user->isAdministrator()) { 9 return true;10 }11 12 return null;13}
如果您想拒绝特定类型用户的所有授权检查,那么您可以从 before 方法返回 false。如果返回 null,则授权检查将流向策略方法。
如果类不包含名称与正在检查的能力名称相匹配的方法,则不会调用策略类的 before 方法。
使用策略授权动作
通过用户模型
您的 Laravel 应用程序中包含的 App\Models\User 模型包含两个用于授权动作的有益方法:can 和 cannot。can 和 cannot 方法接收您想要授权的动作名称和相关的模型。例如,让我们确定用户是否有权更新给定的 App\Models\Post 模型。通常,这将在控制器方法中完成
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Post; 6use Illuminate\Http\RedirectResponse; 7use Illuminate\Http\Request; 8 9class PostController extends Controller10{11 /**12 * Update the given post.13 */14 public function update(Request $request, Post $post): RedirectResponse15 {16 if ($request->user()->cannot('update', $post)) {17 abort(403);18 }19 20 // Update the post...21 22 return redirect('/posts');23 }24}
如果为给定的模型 注册了策略,则 can 方法将自动调用适当的策略并返回布尔结果。如果没有为模型注册策略,can 方法将尝试调用与给定动作名称匹配的基于闭包的门面。
不需要模型的方法
请记住,某些动作可能对应于不需要模型实例的 create 等策略方法。在这些情况下,您可以将类名传递给 can 方法。类名将用于确定在授权动作时使用哪个策略
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Post; 6use Illuminate\Http\RedirectResponse; 7use Illuminate\Http\Request; 8 9class PostController extends Controller10{11 /**12 * Create a post.13 */14 public function store(Request $request): RedirectResponse15 {16 if ($request->user()->cannot('create', Post::class)) {17 abort(403);18 }19 20 // Create the post...21 22 return redirect('/posts');23 }24}
通过 Gate 门面
除了 App\Models\User 模型提供的有用方法外,您还可以随时通过 Gate 门面的 authorize 方法授权动作。
像 can 方法一样,此方法接受您想要授权的动作名称和相关的模型。如果动作未被授权,authorize 方法将抛出 Illuminate\Auth\Access\AuthorizationException 异常,Laravel 异常处理程序将自动将其转换为具有 403 状态代码的 HTTP 响应
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Post; 6use Illuminate\Http\RedirectResponse; 7use Illuminate\Http\Request; 8use Illuminate\Support\Facades\Gate; 9 10class PostController extends Controller11{12 /**13 * Update the given blog post.14 *15 * @throws \Illuminate\Auth\Access\AuthorizationException16 */17 public function update(Request $request, Post $post): RedirectResponse18 {19 Gate::authorize('update', $post);20 21 // The current user can update the blog post...22 23 return redirect('/posts');24 }25}
不需要模型的方法
如前所述,某些像 create 这样的策略方法不需要模型实例。在这些情况下,您应该将类名传递给 authorize 方法。类名将用于确定在授权动作时使用哪个策略
1use App\Models\Post; 2use Illuminate\Http\RedirectResponse; 3use Illuminate\Http\Request; 4use Illuminate\Support\Facades\Gate; 5 6/** 7 * Create a new blog post. 8 * 9 * @throws \Illuminate\Auth\Access\AuthorizationException10 */11public function create(Request $request): RedirectResponse12{13 Gate::authorize('create', Post::class);14 15 // The current user can create blog posts...16 17 return redirect('/posts');18}
通过中间件
Laravel 包含一个中间件,可以在传入请求到达您的路由或控制器之前授权动作。默认情况下,Illuminate\Auth\Middleware\Authorize 中间件可以使用 can 中间件别名 附加到路由,该别名由 Laravel 自动注册。让我们看一个使用 can 中间件来授权用户可以更新帖子的示例
1use App\Models\Post;2 3Route::put('/post/{post}', function (Post $post) {4 // The current user may update the post...5})->middleware('can:update,post');
在此示例中,我们向 can 中间件传递两个参数。第一个是我们想要授权的动作名称,第二个是我们想要传递给策略方法的路由参数。在这种情况下,因为我们使用的是 隐式模型绑定,所以会将一个 App\Models\Post 模型传递给策略方法。如果用户无权执行给定的动作,中间件将返回一个 403 状态代码的 HTTP 响应。
为了方便起见,您还可以使用 can 方法将 can 中间件附加到您的路由
1use App\Models\Post;2 3Route::put('/post/{post}', function (Post $post) {4 // The current user may update the post...5})->can('update', 'post');
如果您使用的是 控制器中间件属性,则可以通过 Authorize 属性应用 can 中间件
1use Illuminate\Routing\Attributes\Controllers\Authorize;2 3#[Authorize('update', 'post')]4public function update(Post $post)5{6 // The current user may update the post...7}
不需要模型的方法
再次说明,一些像 create 这样的策略方法不需要模型实例。在这些情况下,您可以将类名传递给中间件。类名将用于确定在授权动作时使用哪个策略
1Route::post('/post', function () {2 // The current user may create posts...3})->middleware('can:create,App\Models\Post');
在字符串中间件定义中指定完整的类名可能会变得很麻烦。因此,您可以选择使用 can 方法将 can 中间件附加到您的路由
1use App\Models\Post;2 3Route::post('/post', function () {4 // The current user may create posts...5})->can('create', Post::class);
通过 Blade 模板
在编写 Blade 模板时,您可能希望仅在用户有权执行给定动作时才显示页面的某一部分。例如,您可能希望仅在用户确实可以更新帖子时才显示博客文章的更新表单。在这种情况下,您可以使用 @can 和 @cannot 指令
1@can('update', $post) 2 <!-- The current user can update the post... --> 3@elsecan('create', App\Models\Post::class) 4 <!-- The current user can create new posts... --> 5@else 6 <!-- ... --> 7@endcan 8 9@cannot('update', $post)10 <!-- The current user cannot update the post... -->11@elsecannot('create', App\Models\Post::class)12 <!-- The current user cannot create new posts... -->13@endcannot
这些指令是编写 @if 和 @unless 语句的便捷快捷方式。上面的 @can 和 @cannot 语句等效于以下语句
1@if (Auth::user()->can('update', $post))2 <!-- The current user can update the post... -->3@endif4 5@unless (Auth::user()->can('update', $post))6 <!-- The current user cannot update the post... -->7@endunless
您还可以确定用户是否有权执行给定动作数组中的任何动作。要实现这一点,请使用 @canany 指令
1@canany(['update', 'view', 'delete'], $post)2 <!-- The current user can update, view, or delete the post... -->3@elsecanany(['create'], \App\Models\Post::class)4 <!-- The current user can create a post... -->5@endcanany
不需要模型的方法
像大多数其他授权方法一样,如果动作不需要模型实例,您可以将类名传递给 @can 和 @cannot 指令
1@can('create', App\Models\Post::class)2 <!-- The current user can create posts... -->3@endcan4 5@cannot('create', App\Models\Post::class)6 <!-- The current user can't create posts... -->7@endcannot
提供额外上下文
使用策略授权动作时,可以将数组作为第二个参数传递给各种授权函数和帮助程序。数组中的第一个元素将用于确定应调用哪个策略,而数组的其余元素将作为参数传递给策略方法,并在进行授权决策时用于提供额外的上下文。例如,考虑以下包含额外 $category 参数的 PostPolicy 方法定义
1/**2 * Determine if the given post can be updated by the user.3 */4public function update(User $user, Post $post, int $category): bool5{6 return $user->id === $post->user_id &&7 $user->canUpdateCategory($category);8}
当尝试确定经过身份验证的用户是否可以更新给定帖子时,我们可以像这样调用此策略方法
1/** 2 * Update the given blog post. 3 * 4 * @throws \Illuminate\Auth\Access\AuthorizationException 5 */ 6public function update(Request $request, Post $post): RedirectResponse 7{ 8 Gate::authorize('update', [$post, $request->category]); 9 10 // The current user can update the blog post...11 12 return redirect('/posts');13}
授权与 Inertia
尽管授权必须始终在服务器端处理,但提供授权数据给前端应用程序以正确渲染应用程序 UI 通常很方便。Laravel 没有为向 Inertia 驱动的前端公开授权信息定义强制性的约定。
但是,如果您使用的是 Laravel 的 Inertia 驱动的 入门套件 之一,您的应用程序已经包含一个 HandleInertiaRequests 中间件。在此中间件的 share 方法中,您可以返回将提供给应用程序中所有 Inertia 页面的共享数据。此共享数据可以作为一个方便的位置来定义用户的授权信息
1<?php 2 3namespace App\Http\Middleware; 4 5use App\Models\Post; 6use Illuminate\Http\Request; 7use Inertia\Middleware; 8 9class HandleInertiaRequests extends Middleware10{11 // ...12 13 /**14 * Define the props that are shared by default.15 *16 * @return array<string, mixed>17 */18 public function share(Request $request)19 {20 return [21 ...parent::share($request),22 'auth' => [23 'user' => $request->user(),24 'permissions' => [25 'post' => [26 'create' => $request->user()->can('create', Post::class),27 ],28 ],29 ],30 ];31 }32}