跳至内容

授权

介绍

除了提供内置的 身份验证 服务外,Laravel 还提供了一种简单的方法来针对给定资源授权用户操作。例如,即使用户已通过身份验证,他们也可能没有被授权更新或删除应用程序管理的某些 Eloquent 模型或数据库记录。Laravel 的授权功能提供了一种简单、有条理的方式来管理这些类型的授权检查。

Laravel 提供两种主要的授权操作方式: 策略。将门和策略想象成路由和控制器。门提供了一种简单、基于闭包的授权方法,而策略(就像控制器一样)围绕特定模型或资源分组逻辑。在本文档中,我们将首先探讨门,然后检查策略。

在构建应用程序时,你不必选择仅使用门或仅使用策略。大多数应用程序很可能包含门和策略的混合,这完全没问题!门最适用于与任何模型或资源无关的操作,例如查看管理员仪表板。相反,当你希望为特定模型或资源授权操作时,应该使用策略。

编写门

exclamation

门是学习 Laravel 授权功能基础知识的好方法;但是,在构建强大的 Laravel 应用程序时,你应该考虑使用 策略 来组织你的授权规则。

门只是简单的闭包,用于确定用户是否有权执行给定操作。通常,门是在 App\Providers\AppServiceProvider 类的 boot 方法中使用 Gate 外观定义的。门总是接收用户实例作为其第一个参数,并且可以选择接收其他参数,例如相关的 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 外观提供的 allowsdenies 方法。请注意,你不必将当前经过身份验证的用户传递给这些方法。Laravel 会自动将用户传递到门闭包中。通常在应用程序的控制器中执行需要授权的操作之前调用门授权方法

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
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...
}

你可以使用 anynone 方法同时授权多个操作

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 外观上使用 authorize 方法。 AuthorizationException 的实例会自动被 Laravel 转换为 403 HTTP 响应

Gate::authorize('update-post', $post);
 
// The action is authorized...

提供额外的上下文

用于授权能力(allowsdeniescheckanynoneauthorizecancannot)的门方法以及授权 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::allows 方法仍将返回一个简单的布尔值;但是,你可以使用 Gate::inspect 方法获取门返回的完整授权响应

$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...

自定义 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 应用程序的常见模式,因此为了方便起见提供了 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;
}
});

before 方法类似,如果 after 闭包返回非空结果,则该结果将被视为授权检查的结果。

内联授权

有时,你可能希望确定当前经过身份验证的用户是否有权执行给定操作,而无需编写与该操作相对应的专用门。Laravel 允许你通过 Gate::allowIfGate::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);
}

编写策略

策略方法

注册策略类后,您可以为其授权的每个操作添加方法。例如,让我们在 `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` 操作的方法。

lightbulb

所有策略都是通过 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...

自定义 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 应用程序的常见模式,因此为了方便起见提供了 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`,授权检查将继续进行到策略方法。

exclamation

如果类不包含与要检查的功能名称匹配的名称的方法,则不会调用策略类的 `before` 方法。

使用策略授权操作

通过用户模型

与 Laravel 应用程序一起提供的 `App\Models\User` 模型包含两种有用的方法来授权操作:`can` 和 `cannot`。`can` 和 `cannot` 方法接收您希望授权的操作的名称和相关模型。例如,让我们确定用户是否有权更新给定的 `App\Models\Post` 模型。通常,这将在控制器方法中完成。

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
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` 方法将尝试调用与给定操作名称匹配的基于闭包的网关。

不需要模型的操作

请记住,某些操作可能对应于像 `create` 这样的策略方法,这些方法不需要模型实例。在这些情况下,您可以将类名传递给 `can` 方法。类名将用于确定在授权操作时使用哪个策略。

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
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\Http\Controllers\Controller;
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 模板

在编写 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

提供额外的上下文

在使用策略授权操作时,您可以将数组作为第二个参数传递给各种授权函数和帮助程序。数组中的第一个元素将用于确定应调用哪个策略,而数组其余元素将作为参数传递给策略方法,并且可以在做出授权决定时用于附加上下文。例如,请考虑以下包含附加 `$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),
],
],
],
];
}
}