Skip to content

授权

介绍

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

Laravel 提供了两种主要的授权操作方式:拦截器和策略。将 gate 和策略视为 routes 和 controllers。拦截器提供了一种简单的、基于闭包的授权方法,而策略(如控制器)则围绕特定模型或资源对逻辑进行分组。在本文档中,我们将首先探索 Gate,然后检查策略。

在构建应用程序时,您无需在专拦截器使用 Gate 或专拦截器使用 Policies 之间进行选择。大多数应用程序很可能包含一些 gate 和 policy 的混合,这完全没问题!拦截器最适用于与任何模型或资源无关的操作,例如查看管理员控制面板。相反,当您希望为特定模型或资源授权操作时,应使用策略。

拦截器

Writing Gates

WARNING

拦截器是学习 Laravel 授权功能基础知识的好方法;但是,在构建健壮的 Laravel 应用程序时,您应该考虑使用策略来组织您的授权规则。

拦截器只是确定用户是否有权执行给定操作的闭包。通常,拦截器是使用 Gate 拦截器面在 App\Providers\AppServiceProvider 类的 boot 方法中定义的。Gates 始终接收用户实例作为其第一个参数,并且可以选择接收其他参数,例如相关的 Eloquent 模型。

在此示例中,我们将定义一个入口来确定用户是否可以更新给定的 App\Models\Post 模型。该 gate 将通过将用户的 id 与创建帖子的用户的 user_id 进行比较来实现这一点:

php
    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;
        });
    }

与控制器一样,拦截器也可以使用类回调数组定义:

php
    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提供的 allowsdenies 方法。请注意,您不需要将当前经过身份验证的用户传递给这些方法。Laravel 将自动负责将用户传递到 gate closure 中。在执行需要授权的操作之前,通常会在应用程序的控制器中调用 gate 授权方法:

php
    <?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
    {
        /**
         * 更新给定的帖子
         */
        public function update(Request $request, Post $post): RedirectResponse
        {
            if (! Gate::allows('update-post', $post)) {
                abort(403);
            }

            // 更新帖子...

            return redirect('/posts');
        }
    }

如果您想确定当前经过身份验证的用户以外的用户是否有权执行操作,您可以在 Gate Facade上使用 forUser 方法:

php
    if (Gate::forUser($user)->allows('update-post', $post)) {
        // 用户可以更新帖子...
    }

    if (Gate::forUser($user)->denies('update-post', $post)) {
        // 用户无法更新帖子...
    }

您可以使用 anynone 方法一次授权多个操作:

php
    if (Gate::any(['update-post', 'delete-post'], $post)) {
        // 用户可以更新或删除帖子...
    }

    if (Gate::none(['update-post', 'delete-post'], $post)) {
        // 用户无法更新或删除帖子...
    }

授权或引发异常

如果你想尝试授权一个操作,并在不允许用户执行给定操作时自动抛出一个 Illuminate\Auth\Access\AuthorizationException ,你可以使用 Gate facade 的 authorize 方法。AuthorizationException 的实例会被 Laravel 自动转换为 403 HTTP 响应:

php
    Gate::authorize('update-post', $post);

    // 操作已授权...

提供其他上下文

用于授权能力的facade方法(allowsdeniescheckanynoneauthorizecan``、cannot)和授权 Blade 指令@can@cannot@canany)可以接收数组作为其第二个参数。这些数组元素作为参数传递给 gate 闭包,并且可以在做出授权决策时用于额外的上下文:

php
    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])) {
        // 用户可以创建帖子...
    }

拦截器响应

到目前为止,我们只研究了返回简单布尔值的facade。但是,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从您的登机口返回一个 Illuminate\Auth\Access\Response

php
    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 返回的完整授权响应:

php
    $response = Gate::inspect('edit-settings');

    if ($response->allowed()) {
        // 操作已授权...
    } else {
        echo $response->message();
    }

当使用 Gate::authorize 方法时,如果操作未授权,则会抛出 AuthorizationException,授权响应提供的错误信息将传播到 HTTP 响应中:

php
    Gate::authorize('edit-settings');

    // 操作已授权...

自定义 HTTP 响应状态

当通过 Gate 拒绝操作时,将返回 403 HTTP 响应;但是,返回备用 HTTP 状态代码有时可能很有用。您可以使用 Illuminate\Auth\Access\Response 类上的 denyWithStatus 静态构造函数自定义为授权检查失败返回的 HTTP 状态代码:

php
    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 方法:

php
    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 方法来定义一个在所有其他授权检查之前运行的闭包:

php
    use App\Models\User;
    use Illuminate\Support\Facades\Gate;

    Gate::before(function (User $user, string $ability) {
        if ($user->isAdministrator()) {
            return true;
        }
    });

如果 before 闭包返回非 null 结果,则该结果将被视为授权检查的结果。

你可以使用 after 方法来定义一个闭包,在所有其他授权检查之后执行:

php
    use App\Models\User;

    Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
        if ($user->isAdministrator()) {
            return true;
        }
    });

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

内联授权

有时,您可能希望确定当前经过身份验证的用户是否有权执行给定操作,而无需编写与该操作相对应的专用逻辑。Laravel 允许你通过 Gate::allowIfGate::d enyIf 方法执行这些类型的 “内联” 授权检查。内联授权不会执行任何定义的 “before” 或 “after” 授权钩子

php
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\AuthorizationExceptionAuthorizationException 的实例由 Laravel 的异常处理程序自动转换为 403 HTTP 响应。

创建策略

生成策略

策略是围绕特定模型或资源组织授权逻辑的类。例如,如果您的应用程序是博客,则您可能有一个 App\Models\Post 模型和相应的 App\Policies\PostPolicy 来授权用户操作,例如创建或更新帖子。

您可以使用 make:policy Artisan 命令生成策略。生成的策略将放置在 app/Policies 目录中。如果您的应用程序中不存在此目录,Laravel 将为您创建它:

shell
php artisan make:policy PostPolicy

make:policy 命令将生成一个空的策略类。如果您想生成一个类,其中包含与查看、创建、更新和删除资源相关的示例策略方法,则可以在执行命令时提供 --model 选项:

shell
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 方法注册自定义策略发现回调。通常,应从应用程序的 AppServiceProviderboot 方法中调用此方法:

php
    use Illuminate\Support\Facades\Gate;

    Gate::guessPolicyNamesUsing(function (string $modelClass) {
        // 返回给定模型的策略类的名称...
    });

手动注册策略

使用 Gate Facade,你可以在应用程序的 AppServiceProviderboot 方法中手动注册策略及其相应的模型:

php
    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 方法将接收 UserPost 实例作为其参数,并且应返回 truefalse,指示用户是否有权更新给定的 Post。因此,在此示例中,我们将验证用户的 ID 是否与帖子上的 user_id 匹配:

php
    <?php

    namespace App\Policies;

    use App\Models\Post;
    use App\Models\User;

    class PostPolicy
    {
        /**
         * 确定用户是否可以更新给定的帖子。
         */
        public function update(User $user, Post $post): bool
        {
            return $user->id === $post->user_id;
        }
    }

您可以根据需要在策略上定义其他方法,以执行策略授权的各种操作。例如,您可以定义 viewdelete 方法来授权各种与 Post 相关的操作,但请记住,您可以自由地为策略方法指定任何您喜欢的名称。

如果在通过 Artisan 控制台生成策略时使用 --model 选项,则它已经包含 viewAnyviewcreateupdatedeleterestoreforceDelete 操作的方法。

NOTE

所有策略都通过 Laravel 服务容器解析,允许您在策略的构造函数中键入提示任何需要的依赖项,以便自动注入它们。

策略响应

到目前为止,我们只研究了返回简单布尔值的策略方法。但是,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从策略方法返回一个 Illuminate\Auth\Access\Response 实例:

php
    use App\Models\Post;
    use App\Models\User;
    use Illuminate\Auth\Access\Response;

    /**
     * 确定用户是否可以更新给定的帖子。
     */
    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 返回的完整授权响应:

php
    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 响应中:

php
    Gate::authorize('update', $post);

    // The action is authorized...

自定义 HTTP 响应状态

当通过策略方法拒绝操作时,将返回 403 HTTP 响应;但是,返回备用 HTTP 状态代码有时可能很有用。您可以使用 Illuminate\Auth\Access\Response 类上的 denyWithStatus 静态构造函数自定义为授权检查失败返回的 HTTP 状态代码:

php
    use App\Models\Post;
    use App\Models\User;
    use Illuminate\Auth\Access\Response;

    /**
     * 确定用户是否可以更新给定的帖子。
     */
    public function update(User $user, Post $post): Response
    {
        return $user->id === $post->user_id
                    ? Response::allow()
                    : Response::denyWithStatus(404);
    }

因为通过 404 响应隐藏资源是 Web 应用程序的常见模式,所以为了方便起见,提供了 denyAsNotFound 方法:

php
    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();
    }

没有模型的方法

某些策略方法仅接收当前经过身份验证的用户的实例。这种情况在授权创建操作时最为常见。例如,如果您正在创建博客,您可能希望确定用户是否有权创建任何文章。在这些情况下,您的策略方法应该只期望接收一个 user 实例:

php
    /**
     * 确定给定用户是否可以创建帖子
     */
    public function create(User $user): bool
    {
        return $user->role == 'writer';
    }

游客

默认情况下,如果传入的 HTTP 请求不是由经过身份验证的用户发起的,则所有入口和策略都会自动返回 false。但是,您可以通过声明 “optional” 类型提示或为用户参数定义提供 null 默认值来允许这些授权检查传递到您的门和策略:

php
    <?php

    namespace App\Policies;

    use App\Models\Post;
    use App\Models\User;

    class PostPolicy
    {
        /**
         * 确定用户是否可以更新给定的帖子
         */
        public function update(?User $user, Post $post): bool
        {
            return $user?->id === $post->user_id;
        }
    }

策略过滤器

对于某些用户,您可能希望授权给定策略中的所有操作。为此,请在策略上定义 before 方法。before 方法将在策略上的任何其他方法之前执行,让您有机会在实际调用预期的策略方法之前授权操作。此功能最常用于授权应用程序管理员执行任何操作:

php
    use App\Models\User;

    /**
     * 执行预授权检查
     */
    public function before(User $user, string $ability): bool|null
    {
        if ($user->isAdministrator()) {
            return true;
        }

        return null;
    }

如果要拒绝对特定类型用户的所有授权检查,则可以从 before 方法返回 false。如果返回 null,则授权检查将落入策略方法。

WARNING

如果策略类的 before 方法不包含其名称与正在检查的能力的名称匹配,则不会调用该类的 before

使用策略授权操作

通过用户模型

Laravel 应用程序中包含的 App\Models\User 模型包括两种用于授权操作的有用方法:cancannotcancannot 方法接收要授权的操作的名称和相关模型。例如,让我们确定用户是否有权更新给定的 App\Models\Post 模型。通常,这将在 controller 方法中完成:

php
    <?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
    {
        /**
         * 更新给定的帖子
         */
        public function update(Request $request, Post $post): RedirectResponse
        {
            if ($request->user()->cannot('update', $post)) {
                abort(403);
            }

            // 更新帖子...

            return redirect('/posts');
        }
    }

如果为给定模型注册了策略,则 can 方法将自动调用相应的策略并返回布尔结果。如果没有为模型注册策略,can 方法将尝试调用与给定操作名称匹配的基于闭包的 Gate。

不需要模型的操作

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

php
    <?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
    {
        /**
         * 创建帖子
         */
        public function store(Request $request): RedirectResponse
        {
            if ($request->user()->cannot('create', Post::class)) {
                abort(403);
            }

            // 创建帖子...

            return redirect('/posts');
        }
    }

通过 Gate Facade

除了提供给 App\Models\User 模型的有用方法外,您始终可以通过 Gate facade 的 authorize 方法授权操作。

can 方法一样,此方法接受要授权的操作的名称和相关模型。如果操作未获得授权,authorize 方法将抛出异常 Illuminate\Auth\Access\AuthorizationException ,Laravel 异常处理程序将自动将其转换为具有 403 状态代码的 HTTP 响应:

php
    <?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
    {
        /**
         * 更新给定的博客文章
         *
         * @throws \Illuminate\Auth\Access\AuthorizationException
         */
        public function update(Request $request, Post $post): RedirectResponse
        {
            Gate::authorize('update', $post);

            // 当前用户可以更新博客文章...

            return redirect('/posts');
        }
    }

不需要模型的操作

如前所述,某些策略方法(如 create)不需要模型实例。在这些情况下,您应该将类名传递给 authorize 方法。类名将用于确定在授权操作时要使用的策略:

php
    use App\Models\Post;
    use Illuminate\Http\RedirectResponse;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Gate;

    /**
     * 创建新的博客文章
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function create(Request $request): RedirectResponse
    {
        Gate::authorize('create', Post::class);

        // 当前用户可以创建博客文章...

        return redirect('/posts');
    }

通过中间件验证

Laravel 包含一个中间件,它可以在传入请求到达您的路由或控制器之前授权操作。默认情况下, Illuminate\Auth\Middleware\Authorize 中间件可以使用 can中间件别名附加到路由,该别名由 Laravel 自动注册。让我们探索一个使用 can 中间件授权用户可以更新文章的示例:

php
    use App\Models\Post;

    Route::put('/post/{post}', function (Post $post) {
        // 当前用户可以更新帖子...
    })->middleware('can:update,post');

在此示例中,我们将向 can 中间件传递两个参数。第一个是我们希望授权的操作的名称,第二个是我们希望传递给策略方法的 route 参数。在这种情况下,由于我们使用的是隐式模型绑定,因此 App\Models\Post 模型将传递给策略方法。如果用户无权执行给定的操作,中间件将返回带有 403 状态代码的 HTTP 响应。

为方便起见,您还可以使用 can 方法将 can 中间件附加到您的路由:

php
    use App\Models\Post;

    Route::put('/post/{post}', function (Post $post) {
        // 当前用户可以更新帖子...
    })->can('update', 'post');

不需要模型的操作

同样,某些策略方法(如 create)不需要模型实例。在这些情况下,你可以将类名传递给中间件。类名将用于确定在授权操作时要使用的策略:

php
    Route::post('/post', function () {
        // 当前用户可以创建帖子...
    })->middleware('can:create,App\Models\Post');

在字符串中间件定义中指定整个类名可能会变得很麻烦。因此,你可以选择使用 can 方法将 can 中间件附加到你的路由:

php
    use App\Models\Post;

    Route::post('/post', function () {
        // 当前用户可以创建帖子...
    })->can('create', Post::class);

通过 Blade 模板

在编写 Blade 模板时,您可能希望仅在用户有权执行给定操作时显示页面的一部分。例如,您可能希望仅在用户实际可以更新博客文章时才显示博客文章的更新表单。在这种情况下,您可以使用 @can@cannot 指令:

blade
@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 语句等效于以下语句:

blade
@if (Auth::user()->can('update', $post))
    <!-- 当前用户可以更新帖子... -->
@endif

@unless (Auth::user()->can('update', $post))
    <!-- 当前用户无法更新帖子... -->
@endunless

您还可以确定用户是否有权从给定的操作数组中执行任何操作。为此,请使用 @canany 指令:

blade
@canany(['update', 'view', 'delete'], $post)
    <!-- 当前用户可以更新、查看或删除帖子... -->
@elsecanany(['create'], \App\Models\Post::class)
    <!-- 当前用户可以创建帖子... -->
@endcanany

不需要模型的操作

与大多数其他授权方法一样,如果操作不需要模型实例,则可以将类名传递给 @can@cannot 指令:

blade
@can('create', App\Models\Post::class)
    <!-- 当前用户可以创建帖子... -->
@endcan

@cannot('create', App\Models\Post::class)
    <!-- 当前用户无法创建帖子... -->
@endcannot

提供其他上下文

使用策略授权操作时,您可以将数组作为第二个参数传递给各种授权函数和帮助程序。数组中的第一个元素将用于确定应调用哪个策略,而其余数组元素将作为参数传递给策略方法,并可用于在做出授权决策时的其他上下文。例如,考虑以下 PostPolicy 方法定义,其中包含一个额外的 $category 参数:

php
    /**
     * 确定用户是否可以更新给定的帖子
     */
    public function update(User $user, Post $post, int $category): bool
    {
        return $user->id === $post->user_id &&
               $user->canUpdateCategory($category);
    }

当尝试确定经过身份验证的用户是否可以更新给定的帖子时,我们可以调用此策略方法,如下所示:

php
    /**
     * 更新给定的博客文章
     *
     * @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
<?php

namespace App\Http\Middleware;

use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    // ...

    /**
     * 定义默认共享的 props
     *
     * @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),
                    ],
                ],
            ],
        ];
    }
}