Skip to content

授权

介绍

除了提供开箱即用的认证服务外,Laravel 还提供了一种简单的方法来对用户操作进行授权。与认证类似,Laravel 的授权方法也很简单,主要有两种授权操作的方法:门卫和策略。

可以将门卫和策略视为路由和控制器。门卫提供了一种简单的基于闭包的授权方法,而策略则像控制器一样,将其逻辑围绕特定的模型或资源进行分组。我们将首先探讨门卫,然后研究策略。

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

门卫

编写门卫

门卫是确定用户是否有权执行给定操作的闭包,通常在 App\Providers\AuthServiceProvider 类中使用 Gate facade 定义。门卫总是接收用户实例作为第一个参数,并可以选择接收其他参数,例如相关的 Eloquent 模型:

php
/**
 * 注册任何认证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Gate::define('edit-settings', function ($user) {
        return $user->isAdmin;
    });

    Gate::define('update-post', function ($user, $post) {
        return $user->id == $post->user_id;
    });
}

门卫也可以使用 Class@method 风格的回调字符串定义,类似于控制器:

php
/**
 * 注册任何认证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Gate::define('update-post', 'App\Policies\PostPolicy@update');
}

授权操作

要使用门卫授权操作,您应该使用 allowsdenies 方法。请注意,您不需要将当前认证的用户传递给这些方法。Laravel 会自动处理将用户传递给门卫闭包:

php
if (Gate::allows('edit-settings')) {
    // 当前用户可以编辑设置
}

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

if (Gate::denies('update-post', $post)) {
    // 当前用户不能更新帖子...
}

如果您想确定特定用户是否有权执行某个操作,可以在 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)) {
    // 用户不能更新或删除帖子
}

拦截门卫检查

有时,您可能希望为特定用户授予所有权限。您可以使用 before 方法定义一个在所有其他授权检查之前运行的回调:

php
Gate::before(function ($user, $ability) {
    if ($user->isSuperAdmin()) {
        return true;
    }
});

如果 before 回调返回非空结果,该结果将被视为检查的结果。

您可以使用 after 方法定义一个在所有其他授权检查之后执行的回调:

php
Gate::after(function ($user, $ability, $result, $arguments) {
    if ($user->isSuperAdmin()) {
        return true;
    }
});

before 检查类似,如果 after 回调返回非空结果,该结果将被视为检查的结果。

创建策略

生成策略

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

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

php
php artisan make:policy PostPolicy

make:policy 命令将生成一个空的策略类。如果您希望在类中包含基本的“CRUD”策略方法,可以在执行命令时指定 --model

php
php artisan make:policy PostPolicy --model=Post
lightbulb

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

注册策略

策略存在后,需要注册。包含在新鲜 Laravel 应用程序中的 AuthServiceProvider 包含一个 policies 属性,该属性将您的 Eloquent 模型映射到其相应的策略。注册策略将指示 Laravel 在对给定模型授权操作时使用哪个策略:

php
<?php

namespace App\Providers;

use App\Post;
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 应用程序的策略映射。
     *
     * @var array
     */
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    /**
     * 注册任何应用程序认证/授权服务。
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        //
    }
}

策略自动发现

Laravel 可以自动发现策略,只要模型和策略遵循标准的 Laravel 命名约定。具体来说,策略必须位于包含模型的目录下的 Policies 目录中。因此,例如,模型可以放置在 app 目录中,而策略可以放置在 app/Policies 目录中。此外,策略名称必须与模型名称匹配,并具有 Policy 后缀。因此,User 模型将对应于 UserPolicy 类。

如果您希望提供自己的策略发现逻辑,可以使用 Gate::guessPolicyNamesUsing 方法注册自定义回调。通常,此方法应从应用程序的 AuthServiceProviderboot 方法中调用:

php
use Illuminate\Support\Facades\Gate;

Gate::guessPolicyNamesUsing(function ($modelClass) {
    // 返回策略类名...
});
exclamation

在您的 AuthServiceProvider 中显式映射的任何策略将优先于任何可能自动发现的策略。

编写策略

策略方法

策略注册后,您可以为其授权的每个操作添加方法。例如,让我们在我们的 PostPolicy 上定义一个 update 方法,该方法确定给定的 User 是否可以更新给定的 Post 实例。

update 方法将接收一个 User 和一个 Post 实例作为其参数,并应返回 truefalse,指示用户是否有权更新给定的 Post。因此,对于此示例,让我们验证用户的 id 是否与帖子的 user_id 匹配:

php
<?php

namespace App\Policies;

use App\User;
use App\Post;

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

您可以根据需要继续在策略上定义其他方法,以授权其授权的各种操作。例如,您可以定义 viewdelete 方法来授权各种 Post 操作,但请记住,您可以自由地为策略方法命名。

lightbulb

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

无模型的方法

某些策略方法仅接收当前认证的用户,而不接收它们授权的模型实例。这种情况在授权 create 操作时最为常见。例如,如果您正在创建一个博客,您可能希望检查用户是否有权创建任何帖子。

在定义不会接收模型实例的策略方法时,例如 create 方法,它将不会接收模型实例。相反,您应该将方法定义为仅期望认证用户:

php
/**
 * 确定给定用户是否可以创建帖子。
 *
 * @param  \App\User  $user
 * @return bool
 */
public function create(User $user)
{
    //
}

访客用户

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

php
<?php

namespace App\Policies;

use App\User;
use App\Post;

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

策略过滤器

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

php
public function before($user, $ability)
{
    if ($user->isSuperAdmin()) {
        return true;
    }
}

如果您希望为用户拒绝所有授权,您应该从 before 方法返回 false。如果返回 null,授权将传递到策略方法。

exclamation

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

使用策略授权操作

通过用户模型

Laravel 应用程序中包含的 User 模型包括两个用于授权操作的有用方法:cancantcan 方法接收您希望授权的操作和相关模型。例如,让我们确定用户是否有权更新给定的 Post 模型:

php
if ($user->can('update', $post)) {
    //
}

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

不需要模型的操作

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

php
use App\Post;

if ($user->can('create', Post::class)) {
    // 执行相关策略上的“create”方法...
}

通过中间件

Laravel 包含一个中间件,可以在传入请求到达您的路由或控制器之前授权操作。默认情况下,Illuminate\Auth\Middleware\Authorize 中间件在您的 App\Http\Kernel 类中分配了 can 键。让我们探讨一个使用 can 中间件授权用户可以更新博客帖子的示例:

php
use App\Post;

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

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

不需要模型的操作

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

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

通过控制器助手

除了提供给 User 模型的有用方法外,Laravel 还为任何扩展 App\Http\Controllers\Controller 基类的控制器提供了一个有用的 authorize 方法。与 can 方法类似,此方法接受您希望授权的操作名称和相关模型。如果操作未被授权,authorize 方法将抛出一个 Illuminate\Auth\Access\AuthorizationException,默认的 Laravel 异常处理程序将其转换为带有 403 状态码的 HTTP 响应:

php
<?php

namespace App\Http\Controllers;

use App\Post;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * 更新给定的博客帖子。
     *
     * @param  Request  $request
     * @param  Post  $post
     * @return Response
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        // 当前用户可以更新博客帖子...
    }
}

不需要模型的操作

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

php
/**
 * 创建一个新的博客帖子。
 *
 * @param  Request  $request
 * @return Response
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function create(Request $request)
{
    $this->authorize('create', Post::class);

    // 当前用户可以创建博客帖子...
}

授权资源控制器

如果您正在使用资源控制器,可以在控制器的构造函数中使用 authorizeResource 方法。此方法将适当的 can 中间件定义附加到资源控制器的方法。

authorizeResource 方法接受模型的类名作为第一个参数,以及包含模型 ID 的路由/请求参数的名称作为第二个参数:

php
<?php

namespace App\Http\Controllers;

use App\Post;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Post::class, 'post');
    }
}

以下控制器方法将映射到其相应的策略方法:

控制器方法策略方法
showview
createcreate
storecreate
editupdate
updateupdate
destroydelete
lightbulb

您可以使用 --model 选项与 make:policy 命令一起快速为给定模型生成策略类:php artisan make:policy PostPolicy --model=Post

通过 Blade 模板

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

php
@can('update', $post)
    <!-- 当前用户可以更新帖子 -->
@elsecan('create', App\Post::class)
    <!-- 当前用户可以创建新帖子 -->
@endcan

@cannot('update', $post)
    <!-- 当前用户不能更新帖子 -->
@elsecannot('create', App\Post::class)
    <!-- 当前用户不能创建新帖子 -->
@endcannot

这些指令是编写 @if@unless 语句的便捷快捷方式。上面的 @can@cannot 语句分别转换为以下语句:

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

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

您还可以确定用户是否具有给定权限列表中的任何授权能力。为此,请使用 @canany 指令:

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

不需要模型的操作

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

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

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