授权

简介

除了提供开箱即用的 身份认证 服务之外,Laravel 还提供了一种简单的方式授权用户对给定资源的操作。与身份认证一样,Laravel 的授权方法很简单,主要有两种授权操作的方式:Gates 和策略。

可以把 Gates 和策略想象成路由和控制器。Gates 提供了简单、基于闭包的授权方式,策略与控制器类似,将特定模型或资源相关的授权逻辑进行分组。我们先研究 Gates,然后研究策略。

在构建应用时,不用选择只使用 Gates 或策略。大多数应用很可能会把 Gates 和策略混合使用,这完全没问题!Gate 大多情况下用在操作没有关联任何模型或资源的地方,例如查看管理员的后台面板。相反,策略应在对特定的模型或资源授权操作时使用。

Gates

编写 Gates

Gates 是用来判断用户是否授权执行给定操作的闭包,并通常使用 Gate Facade 在 App\Providers\AuthServiceProvider 类中定义。Gates 始终接收一个用户实例作为其第一个参数,并接收可选的其它参数,如相关的 Eloquent 模型:

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

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

Gates 也可以使用 Class@method 风格的回调字符串来定义,比如控制器:

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

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

资源 Gates

还可以使用 resource 方法一次定义多个 Gate 能力:

Gate::resource('posts', 'App\Policies\PostPolicy');

以上等同于手动定义如下 Gate:

Gate::define('posts.view', 'App\Policies\PostPolicy@view');
Gate::define('posts.create', 'App\Policies\PostPolicy@create');
Gate::define('posts.update', 'App\Policies\PostPolicy@update');
Gate::define('posts.delete', 'App\Policies\PostPolicy@delete');

默认情况下,viewcreateupdatedelete 能力会被定义。可以通过将数组作为第三个参数传递给 resource 方法来覆盖默认的能力。数组的键定义能力的名称,而值定义方法名称。例如,以下代码只会创建两个新的 Gate 定义 —— posts.imageposts.photo

Gate::resource('posts', 'PostPolicy', [
    'image' => 'updateImage',
    'photo' => 'updatePhoto',
]);

授权操作

使用 Gates 授权操作时,应使用 allowsdenies 方法。您不需要将当前认证用户传递给这些方法。Laravel 会自动将其传递到 Gate 闭包:

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

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

如果要判断指定用户是否授权进行某操作,可以使用 Gate Facade 的 forUser 方法:

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

if (Gate::forUser($user)->denies('update-post', $post)) {
    // 用户不能更新文章
}

拦截 Gate 检查

有时,可能希望为指定用户授权所有能力。可以使用 before 方法定义在所有其它授权检查之前运行的回调:

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

如果 before 回调返回一个非 null 的结果,该结果会被视为检查结果。

可以使用 after 方法定义每此授权检查后都要执行的回调。但是,不能在 after 回调中修改授权检查的结果:

Gate::after(function ($user, $ability, $result, $arguments) {
    //
});

创建策略

生成策略

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

可以使用 make:policy Artisan 命令 生成策略。生成的策略将位于 app/Policies 目录。如果应用中不存在此目录,Laravel 会自动创建:

php artisan make:policy PostPolicy

make:policy 命令会生成一个空的策略类。如果希望生成的类中包含基本的「CURD」策略方法,可以在运行命令时指定 --model 选项:

php artisan make:policy PostPolicy --model=Post

所有策略都通过 服务容器 解析,因此可以在策略的构造函数中对任何需要的依赖使用类型提示,它们会被自动注入。

注册策略

策略存在后,就需要注册。新 Laravel 应用的 AuthServiceProvider 中包含一个 policies 属性,该属性将 Eloquent 模型映射到与之对应的授权策略。注册策略会指示 Laravel 对给定模型授权时使用哪个策略。

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

        //
    }
}

编写策略

策略方法

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

update 方法接收一个 User 和一个 Post 实例作为其参数,并应返回 truefalse 以指明该用户是否授权更新给定的 Post。因此,在本例中,我们会判断用户的 id 是否和博客文章的 user_id 相匹配:

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 的多个操作,并且可以随意为策略方法指定任何喜欢的名字。

如果通过 Artisan 终端命令生成策略时使用了 --model 选项,那么已经包含了 viewcreateupdatedelete 操作的方法。

不要模型的方法

一些策略方法仅接收当前认证用户实例,而不要它们授权的模型实例。最常见的情况就是授权 create 操作。例如,如果在创建一篇博客,可能希望检查用户是否授权创建任何博客。

当定义不要模型实例的策略方法(例如 create 方法)时,它不会接收模型实例。相反,应该定义仅接收认证用户的方法:

/**
 * 判断给定用户是否可以创建博客文章
 *
 * @param  \App\User  $user
 * @return bool
 */
public function create(User $user)
{
    //
}

访客用户

默认情况下,如果传入的 HTTP 请求不是认证用户发起的,所有 Gates 和策略都会自动返回 false。但是,可以通过声明一个「可选」类型提示或在定义用户参数时提供默认值 null,来允许这些授权检查经过 Gates 和策略:

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 方法会在策略的任何方法之前执行,使您有机会在实际调用预期的策略方法之前授权该操作。此功能常用于授权应用管理员执行任何操作:

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

如果要拒绝用户的所有授权,应该在 before 方法中返回 false。如果返回 null,授权会经过策略方法。

如果策略类不包含和被检查的能力名称相匹配的方法名,那么 before 方法不会被调用。

使用策略授权操作

通过用户模型

Laravel 应用自带的 User 模型包含两个辅助方法来授权操作:cancantcan 方法接收要授权的操作和相关模型。例如,判断用户是否授权更新给定的 Post 模型:

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

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

不要模型的操作

一些操作(如 create)不要模型实例。这些情况下,可以传递一个类名给 can 方法。该类名将用于判断授权操作时使用哪个策略:

use App\Post;

if ($user->can('create', Post::class)) {
    // 运行对应策略的「create」方法
}

通过中间件

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

use App\Post;

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

在此示例中,我们传递给 can 中间件两个参数。第一个参数是要授权的操作名称,第二个是要传递给策略方法的路由参数。这里由于我们使用了 隐式模型绑定,因此 Post 模型会传递给策略方法。如果用户未授权执行给定操作,该中间件会生成一个 403 状态码的 HTTP 响应。

不要模型的操作

同样,一些操作(如 create)不要模型实例。这些情况下,可以传递一个类名给中间件。该类名将用于判断授权操作时使用哪个策略:

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

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * Update the given blog post.
     *
     * @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 方法。该类名将用于判断授权操作时使用哪个策略:

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

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

通过 Blade 模板

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

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

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

这些指令是编写 @if@unless 语句的快捷操作。上述 @can@cannot 语句会分别转换为以下语句:

@if (Auth::user()->can('update', $post))
    <!-- 当前用户可以更新博客文章 -->
@endif

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

不要模型的操作

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

@can('create', App\Post::class)
    <!-- 当前用户可以创建博客文章 -->
@endcan

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