授权
介绍
除了提供内置的身份验证服务外,Laravel 还提供了一种简单的方法来授权用户针对给定资源的操作。例如,即使用户已通过身份验证,他们也可能无权更新或删除应用程序管理的某些 Eloquent 模型或数据库记录。Laravel 的授权功能提供了一种简单、有序的方式来管理这些类型的授权检查。
Laravel 提供两种主要的授权方式:门控和策略。门控和策略可以理解为路由和控制器。门控提供了一种简单的、基于闭包的授权方法,而策略则像控制器一样,围绕特定的模型或资源对逻辑进行分组。在本文档中,我们将首先探讨门控,然后再探讨策略。
在构建应用程序时,您无需在仅使用门控或仅使用策略之间做出选择。大多数应用程序很可能包含门控和策略的某种Mix,这完全没问题!门控最适用于与任何模型或资源无关的操作,例如查看管理员仪表板。相反,当您希望授权特定模型或资源的操作时,应该使用策略。
盖茨
写作之门
门是学习 Laravel 授权功能基础知识的好方法;但是,在构建强大的 Laravel 应用程序时,您应该考虑使用策略来组织授权规则。
门控机制其实就是一些闭包,用于判断用户是否有权执行特定操作。通常,门控机制在类boot
的方法中App\Providers\AppServiceProvider
使用Gate
外观层 (Facade) 进行定义。门控机制始终接收用户实例作为其第一个参数,并可选地接收其他参数,例如相关的 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}
授权操作
要使用门控授权操作,您应该使用外观层提供的allows
或方法。请注意,您无需将当前已验证的用户传递给这些方法。Laravel 会自动将用户传递给门控闭包。通常在执行需要授权的操作之前,在应用程序的控制器中调用门控授权方法:denies
Gate
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}
如果您想确定当前已验证用户以外的用户是否有权执行某项操作,您可以使用外观forUser
上的方法Gate
:
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
Facade 的authorize
方法。Laravel 会自动将 的实例AuthorizationException
转换为 403 HTTP 响应:
1Gate::authorize('update-post', $post);2 3// The action is authorized...
提供额外的上下文
用于授权能力的门控方法(allows
、denies
、check
、any
、none
、authorize
、)和授权Blade 指令(can
、、)可以接收一个数组作为其第二个参数。这些数组元素作为参数传递给门控闭包,并可在进行授权决策时用作附加上下文:cannot
@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 响应状态
当通过 Gate 拒绝某个操作时,403
会返回 HTTP 响应;但是,有时返回其他 HTTP 状态代码也会很有用。您可以使用类denyWithStatus
上的静态构造函数自定义授权检查失败时返回的 HTTP 状态代码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::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
闭包返回非空结果,则该结果将被视为授权检查的结果。
您可以使用该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});
闭包返回的值after
不会覆盖授权检查的结果,除非门或策略返回null
。
内联授权
有时,您可能希望确定当前已验证的用户是否有权执行给定操作,而无需编写与该操作对应的专用门控。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
异常。LaravelAuthorizationException
的异常处理程序会自动将 的实例转换为 403 HTTP 响应。
创建策略
生成策略
策略是围绕特定模型或资源组织授权逻辑的类。例如,如果您的应用是一个博客,您可能有一个App\Models\Post
模型和一个相应的模型,App\Policies\PostPolicy
用于授权用户操作(例如创建或更新帖子)。
您可以使用 Artisan 命令生成策略make:policy
。生成的策略将放置在app/Policies
目录中。如果您的应用程序中不存在此目录,Laravel 将为您创建:
1php artisan make:policy PostPolicy
该make:policy
命令将生成一个空的策略类。如果您希望生成一个包含与查看、创建、更新和删除资源相关的示例策略方法的类,您可以--model
在执行命令时提供一个选项:
1php artisan make:policy PostPolicy --model=Post
注册政策
策略发现
默认情况下,只要模型和策略遵循 Laravel 标准命名约定,Laravel 就会自动发现策略。具体来说,策略必须位于Policies
模型所在目录或更高目录。例如,模型可以放在 目录中,app/Models
而策略可以放在 目录中。在这种情况下,Laravel 会在thenapp/Policies
中查找策略。此外,策略名称必须与模型名称匹配并带有后缀。因此,一个模型对应一个策略类。app/Models/Policies
app/Policies
Policy
User
UserPolicy
如果您想定义自己的策略发现逻辑,可以使用Gate::guessPolicyNamesUsing
方法注册自定义策略发现回调。通常,此方法应从boot
应用程序的 方法中调用AppServiceProvider
:
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
中手动注册策略及其相应的模型:boot
AppServiceProvider
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}
制定政策
策略方法
注册策略类后,您可以为其授权的每个操作添加方法。例如,让我们定义一个update
方法PostPolicy
,用于确定给定操作是否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
相关操作,但请记住,您可以随意为策略方法指定任何名称。
如果您--model
在通过 Artisan 控制台生成策略时使用了该选项,它将已经包含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 状态码也会很有用。您可以使用类denyWithStatus
上的静态构造函数自定义授权检查失败时返回的 HTTP 状态码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::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}
来宾用户
默认情况下,false
如果传入的 HTTP 请求不是由经过身份验证的用户发起的,所有门控和策略都会自动返回。但是,您可以通过声明“可选”类型Prompts或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}
如果您想拒绝特定类型用户的所有授权检查,您可以false
从该before
方法返回。如果null
返回,则授权检查将转交给策略方法。
before
如果策略类不包含名称与所检查能力名称匹配的方法,则不会调用该类的方法
。
使用策略授权操作
通过用户模型
App\Models\User
Laravel 应用程序自带的模型包含两个用于授权操作的实用方法:和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
方法将尝试调用与给定动作名称匹配的基于闭包的 Gate。
不需要模型的操作
请记住,某些操作可能对应于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
别名can
由 Laravel 自动注册。让我们探索一个使用中间件授权用户更新帖子的示例:
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');
不需要模型的操作
同样,某些策略方法(例如 )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
提供额外的上下文
使用策略授权操作时,可以将一个数组作为第二个参数传递给各种授权函数和辅助函数。数组中的第一个元素将用于确定应调用哪个策略,而其余数组元素将作为参数传递给策略方法,并可在制定授权决策时用作附加上下文。例如,考虑以下PostPolicy
包含附加$category
参数的方法定义:
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}
授权与惯性
虽然授权必须始终在服务器上处理,但为了正确呈现应用程序的 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}