除了提供内置的 认证 服务,Laravel 还提供了一种简单的方法来授权用户针对给定资源的操作。例如,即使某个用户已通过身份验证,他们也可能没有权限更新或删除由您的应用程序管理的某些 Eloquent 模型或数据库记录。Laravel 的授权功能提供了一种简单,有组织的方式来管理这些类型的授权检查。
Laravel 提供了两种主要的授权操作方式:门 和 策略。可以将门和策略视为路由和控制器。门提供了一种简单、基于闭包的授权方法,而策略,类似于控制器,将逻辑围绕特定的模型或资源进行分组。在本文档中,我们将首先探讨门,然后再研究策略。
您在构建应用程序时,无需在专门使用门或专门使用策略之间做出选择。大多数应用程序很可能包含门和策略的某种组合,这完全没有问题!门最适用于与任何模型或资源无关的操作,例如查看管理员仪表盘。相比之下,当您希望对特定模型或资源的操作进行授权时,应使用策略。
[!WARNING]
授权门是学习 Laravel 授权功能基础知识的好方法;然而,在构建健壮的 Laravel 应用程序时,你应该考虑使用 策略 来组织你的授权规则。
守卫简单来说就是闭包,用于判断用户是否被授权执行给定操作。通常,守卫在 App\Providers\AppServiceProvider 类的 boot 方法中使用 Gate facade 进行定义。守卫总是将用户实例作为其第一个参数接收,并且可以可选地接收额外的参数,例如相关的 Eloquent 模型。
在此示例中,我们将定义一个门,以确定用户是否可以更新给定的 App\Models\Post 模型。该门将通过比较用户的 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;
});
}像控制器一样,门禁也可以使用类回调数组来定义:
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 门面(facade)提供的 allows 或 denies 方法。请注意,您无需将当前认证的用户传递给这些方法。Laravel 会自动处理将用户传递到“门”的闭包中。通常,您会在执行需要授权的操作之前,在应用程序的控制器中调用“门”的授权方法:
<?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)) {
// The user can update the post...
}
if (Gate::forUser($user)->denies('update-post', $post)) {
// The user can't update the post...
}您可以使用 any 或 none 方法同时授权多个操作:
if (Gate::any(['update-post', 'delete-post'], $post)) {
// The user can update or delete the post...
}
if (Gate::none(['update-post', 'delete-post'], $post)) {
// The user can't update or delete the post...
}如果你想尝试授权一个操作,并且在用户不允许执行给定操作时,自动抛出 Illuminate\Auth\Access\AuthorizationException,你可以使用 Gate facade 的 authorize 方法。AuthorizationException 的实例会被 Laravel 自动转换为 403 HTTP 响应:
Gate::authorize('update-post', $post);
// The action is authorized...用于授权能力的门控方法 (allows, denies, check, any, none, authorize, can, cannot) 以及授权 Blade 指令 (@can, @cannot, @canany) 可以接收一个数组作为它们的第二个参数。这些数组元素会作为参数传递给门控闭包,并可在做出授权决策时用于提供额外上下文:
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])) {
// The user can create the post...
}到目前为止,我们只检查了返回简单布尔值的门。然而,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从您的门返回一个 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()) {
// The action is authorized...
} else {
echo $response->message();
}当使用 Gate::authorize 方法时,该方法在操作未被授权时会抛出 AuthorizationException,授权响应提供的错误消息将传播到 HTTP 响应中:
Gate::authorize('edit-settings');
// The action is authorized...当通过 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 应用程序中如此常见的模式,因此提供了 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();
});有时,您可能希望将所有能力授予特定用户。您可以使用 before 方法来定义一个闭包,该闭包在所有其他授权检查之前运行:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::before(function (User $user, string $ability) {
if ($user->isAdministrator()) {
return true;
}
});如果 before 闭包返回一个非空结果,该结果将被视为授权检查的结果。
您可以使用 after 方法来定义一个闭包,该闭包将在所有其他授权检查之后执行:
use App\Models\User;
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($user->isAdministrator()) {
return true;
}
});after 闭包返回的值不会覆盖授权检查的结果,除非门面或策略返回了 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 响应。
策略是围绕特定模型或资源组织授权逻辑的类。例如,如果你的应用程序是一个博客,你可能有一个 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) {
// Return the name of the policy class for the given model...
});使用 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 操作的方法。
[!NOTE]
所有策略都通过 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 方法来获取由门禁返回的完整授权响应:
use Illuminate\Support\Facades\Gate;
$response = Gate::inspect('update', $post);
if ($response->allowed()) {
// The action is authorized...
} else {
echo $response->message();
}当使用 Gate::authorize 方法时,如果操作未经授权,它会抛出一个 AuthorizationException,此时授权响应提供的错误消息将传播到 HTTP 响应中:
Gate::authorize('update', $post);
// The action is authorized...当一个操作通过策略方法被拒绝时,会返回一个 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 应用程序的一种常见模式,所以提供了 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 请求不是由经过身份验证的用户发起的,所有门和策略都会自动返回 false。然而,您可以通过为用户参数定义声明一个“可选”类型提示或提供一个 null 默认值,来允许这些授权检查透传给您的门和策略:
<?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,授权检查将传递给策略方法。
[!WARNING]
策略类的before方法将不会被调用,如果该类不包含一个名称与被检查的能力名称相匹配的方法。
你的 Laravel 应用中包含的 App\Models\User 模型提供了两个用于授权操作的实用方法:can 和 cannot。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) {
// The current user may update the post...
})->middleware('can:update,post');在此示例中,我们向can中间件传递了两个参数。第一个是我们要授权的操作名称,第二个是我们希望传递给策略方法的路由参数。在这种情况下,由于我们使用的是隐式模型绑定,一个App\Models\Post模型将被传递给策略方法。如果用户未被授权执行给定操作,中间件将返回一个状态码为403的HTTP响应。
为方便起见,您也可以使用 can 方法将 can 中间件附加到您的路由:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->can('update', 'post');再次,某些策略方法像 create 不需要模型实例。在这些情况下,您可以传递一个类名给中间件。该类名将用于确定在授权操作时使用哪个策略:
Route::post('/post', function () {
// The current user may create posts...
})->middleware('can:create,App\Models\Post');在字符串中间件定义中指定完整的类名可能会变得繁琐。基于这个原因,您可以选择使用 can 方法将 can 中间件附加到您的路由上:
use App\Models\Post;
Route::post('/post', function () {
// The current user may create posts...
})->can('create', Post::class);在编写 Blade 模板时,您可能希望仅当用户被授权执行给定操作时,才显示页面的一部分。例如,您可能希望仅当用户确实可以更新博客文章时,才显示该文章的更新表单。在这种情况下,您可以使用 @can 和 @cannot 指令:
@can('update', $post)
<!-- The current user can update the post... -->
@elsecan('create', App\Models\Post::class)
<!-- The current user can create new posts... -->
@else
<!-- ... -->
@endcan
@cannot('update', $post)
<!-- The current user cannot update the post... -->
@elsecannot('create', App\Models\Post::class)
<!-- The current user cannot create new posts... -->
@endcannot这些指令是编写 @if 和 @unless 语句的便捷快捷方式。上述 @can 和 @cannot 语句等同于以下语句:
@if (Auth::user()->can('update', $post))
<!-- The current user can update the post... -->
@endif
@unless (Auth::user()->can('update', $post))
<!-- The current user cannot update the post... -->
@endunless您还可以确定用户是否被授权执行给定操作数组中的任何操作。为此,请使用 @canany 指令:
@canany(['update', 'view', 'delete'], $post)
<!-- The current user can update, view, or delete the post... -->
@elsecanany(['create'], \App\Models\Post::class)
<!-- The current user can create a post... -->
@endcanany与大多数其他授权方法一样,如果操作不需要模型实例,你可以将类名传递给 @can 和 @cannot 指令:
@can('create', App\Models\Post::class)
<!-- The current user can create posts... -->
@endcan
@cannot('create', App\Models\Post::class)
<!-- The current user can't create posts... -->
@endcannot当使用策略授权操作时,您可以将一个数组作为第二个参数传递给各种授权函数和辅助函数。数组中的第一个元素将用于确定应调用哪个策略,而数组的其余元素则作为参数传递给策略方法,并且可以在做出授权决策时用于提供额外的上下文。例如,考虑以下 PostPolicy 方法定义,它包含一个额外的 $category 参数:
/**
* 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');
}尽管授权始终必须在服务器上处理,但通常很方便为你的前端应用程序提供授权数据,以便正确渲染你的应用程序的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),
],
],
],
];
}
}