用户授权

简介

除了内置提供的 用户认证 服务外,Laravel 还提供了用户授权和资源访问控制的方案。有很多种方法与辅助函数能帮你处理授权逻辑,在本文档中我们将会涵盖每一种方式。

定义权限

判断一个用户是否允许执行特定行为,最简单的方式就是使用 Illuminate\Auth\Access\Gate 类定义「权限」。

可以在 AuthServiceProvider 文件中定义应用程序的所有权限。举个例子,我们需要定义一个 update-post 的权限,需要判断目前的 UserPost 模型 是否有所属关系,也就是「文章」是不是「用户」发的,我们会判断用户的 id 与文章的 user_id 是否一致:

<?php

namespace App\Providers;

use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 注册应用程序的认证或授权服务。
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function boot(GateContract $gate)
    {
        $this->registerPolicies($gate);

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

注意,我们并不会检查当指定的 $user 是不是 NULL。未登录用户或是没有用 forUser 方法指定的用户,Gate 会自动为其 所有权限 返回 false

基于类的权限

除了注册 闭包 作为授权的回调,你也可以通过传递包含 类名称方法 的字符串来注册类方法,该类会通过 服务容器 被解析:

$gate->define('update-post', 'Class@method');

拦截授权检查

有时你希望赋予指定用户最高权限,如管理员拥有所有权限,可以使用 before 方法来定义所有授权检查前会被运行的回调:

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

如果 before 的回调返回一个非 null 的结果,则该结果会被作为检查的结果,并中断后面的其他验证。

你还可以使用 after 方法定义一个当所有授权检查后会被运行的回调。但是,你无法修改 after 回调中授权检查的结果:

$gate->after(function ($user, $ability, $result, $arguments) {
    //
});

检查权限

通过 Gate Facade

一旦权限被定义后,我们可以使用不同方式来做「权限检查」。

首先,我们可以使用 Gate facadecheckallowsdenies 方法。所有的这些方法会获取权限的名称及参数,并会被传递至权限的回调中。

需要传递当前登录用户至该方法内,因为 Gate 会自动加载当前登录用户,所以,当通过我们前面定义的 update-post 权限进行检查时,只需传递一个 Post 实例至 denies 方法即可:

<?php

namespace App\Http\Controllers;

use Gate;
use App\User;
use App\Post;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * 更新指定的文章。
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $post = Post::findOrFail($id);

        if (Gate::denies('update-post', $post)) {
            abort(403);
        }

        // 更新文章...
    }
}

allows 方法只是简单的将 denies 方法给颠倒过来,当授权成功时候会返回 truecheck 方法则是 allows 方法的别名。

检查指定用户的权限

如果你想检查 除了当前登录用户以外的其他用户 是否拥有指定的权限,你可以使用 forUser 方法:

if (Gate::forUser($user)->allows('update-post', $post)) {
    //
}

传递多个参数

当然,权限的回调可以传递多个参数:

Gate::define('delete-comment', function ($user, $post, $comment) {
    //
});

如果你的权限需要多个参数,只需简单的传递一个数组作为 Gate 方法的参数:

if (Gate::allows('delete-comment', [$post, $comment])) {
    //
}

通过用户模型

另外,你也可以通过 User 模型的实例检查权限。默认情况下,Laravel 的 App\User 模型使用了 Authorizable trait,它提供了两个方法:cancannot

这些方法使用起来相似于 Gate facade 提供的 allowsdenies 方法。所以,沿用我们之前的例子,可以将代码改成如下:

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * 更新指定的文章。
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return Response
     */
    public function update(Request $request, $id)
    {
        $post = Post::findOrFail($id);

        if ($request->user()->cannot('update-post', $post)) {
            abort(403);
        }

        // 更新文章...
    }
}

当然,can 方法只是简单的将 cannot 方法给颠倒过来:

if ($request->user()->can('update-post', $post)) {
    // 更新文章...
}

使用 Blade 模板

在 Blade 模板中,你还可以使用 @can 命令来快速检查当前登录用户是否有指定的权限。例如:

<a href="/post/{{ $post->id }}">查看文章</a>

@can('update-post', $post)
    <a href="/post/{{ $post->id }}/edit">编辑文章</a>
@endcan

你也可以将 @else 命令配合 @can 命令:

@can('update-post', $post)
    <!-- 当前登录用户可以更新文章 -->
@else
    <!-- 当前登录用户不可以更新文章 -->
@endcan

使用表单请求

你也可以在 表单请求authorize 方法中采用你的 Gate 定义的权限。举个例子:

/**
 * 判断当用户已被授权并发送此请求。
 *
 * @return bool
 */
public function authorize()
{
    $postId = $this->route('post');

    return Gate::allows('update', Post::findOrFail($postId));
}

授权策略

创建授权策略

在大型应用程序中,把你所有的授权逻辑定义在 AuthServiceProvider 中可能成为累赘,你可以切分你的授权逻辑至「授权策略」类。授权策略是简单的 PHP 类,并基于授权的资源将授权逻辑进行分组。

首先,让我们生成一个授权策略来管理 Post 模型的授权。你可以通过 make:policy artisan 命令 生成一个授权策略。生成的授权策略会被放置于 app/Policies 目录中:

php artisan make:policy PostPolicy

注册授权策略

一旦该授权策略存在,我们需要将它与 Gate 类进行注册。AuthServiceProvider 包含了一个 policies 属性,可将各种模型对应至管理它们的授权策略。所以,我们需要指定 Post 模型的授权策略是 PostPilicy 类:

<?php

namespace App\Providers;

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

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

    /**
     * 注册任何应用程序的认证或授权服务。
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function boot(GateContract $gate)
    {
        $this->registerPolicies($gate);
    }
}

编写授权策略

一旦授权策略被生成且注册,我们就可以为每个权限的授权增加方法。例如,让我们在 PostPolicy 中定义一个 update 方法,它会判断指定的 User 是否可以「更新」一条 Post

<?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;
    }
}

你可以接着在此授权策略定义额外的方法,作为各种权限需要的授权。例如,你可以定义 showdestroyaddComment 方法来授权 Post 的多种行为。

注意:所有授权策略会通过 Laravel 服务容器 解析,意指你可以在授权策略的构造器对任何需要的依赖使用类型提示,它们将会被自动注入。

拦截所有检查

有时,你可能希望在授权策略赋予所有权限给指定用户。对于这种情况,只要在授权策略中定义一个 before 方法。授权策略的此方法会在其它所有授权检查前被运行:

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

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

检查授权策略

授权策略方法的调用方式和基于授权的回调闭包是完全相同的。你可以使用Gate facade、User 模型、@can Blade 命令或是 policy 辅助函数。

通过 Gate Facade

Gate 会通过检查传递给该类方法的参数自动判断该使用哪种授权策略。所以,如果我们传递一个 Post 实例到 denies 方法,Gate 会采用对应的 PostPolicy 来授权行为:

<?php

namespace App\Http\Controllers;

use Gate;
use App\User;
use App\Post;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * 更新指定的文章。
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $post = Post::findOrFail($id);

        if (Gate::denies('update', $post)) {
            abort(403);
        }

        // 更新文章...
    }
}

通过用户模型

User 模型的 cancannot 方法也会自动采用指定参数可用的授权策略。此方法提供一个简单的方式在应用程序中为任何获取到的 User 实例授权行为:

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

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

使用 Blade 模板

同样的,@can Blade 命令会采用指定参数可用的授权。

@can('update', $post)
    <!-- 目前的用户可以更新文章 -->
@endcan

通过授权策略辅助函数

全局的 policy 辅助函数可以被用于为指定的类实例获取 Policy 类。例如,我们可以传递一个 Post 实例至 policy 辅助函数,获取对应的 PostPolicy 类实例:

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

控制器授权

默认的,App\Http\Controllers\Controller 类包含了 Laravel 使用的 AuthorizesRequests trait。此 trait 提供了 authorize 方法,它可以被用于快速授权一个指定的行为,当无权限运行该行为时会抛出 HttpException

authorize 方法与其它授权方法共用了同样的特征,像是 Gate::allows$user->can()。所以,让我们使用 authorize 方法来快速授权一个请求以更新一条 Post

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * 更新指定的文章。
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $post = Post::findOrFail($id);

        $this->authorize('update', $post);

        // 更新文章...
    }
}

如果该行为被授权了,控制器将会继续正常运行;但是,如果 authorize 方法判断没有权限运行该行为,那么将会自动生成一个带有 403 Not Authorized 状态码的 HTTP 响应并抛出异常。如你所见,authorize 方法是进行授权行为或处理抛出异常的一个简单、快速的方法,仅使用了一行程序代码。

AuthorizesRequests trait 也提供了 authorizeForUser 方法来为当前非认证用户授权行为:

$this->authorizeForUser($user, 'update', $post);

自动判断授权策略方法

通常,一个授权策略方法会对应一个控制器方法。以下方的 update 方法为例,控制器方法及授权策略方法会共用相同的名称:update

因此,Laravel 让你能够简单的传递实例参数至 authorize 方法,基于被调用的函数名称,自动判断出应该授权的权限。在本例中,因为 authorize 被控制器的 update 方法调用,所以也会调用 PostPolicy 中的 update 方法:

/**
 * 更新指定的文章。
 *
 * @param  int  $id
 * @return Response
 */
public function update($id)
{
    $post = Post::findOrFail($id);

    $this->authorize($post);

    // 更新文章...
}

{note} 欢迎任何形式的转载,但请务必注明出处,尊重他人劳动共创开源社区。

转载请注明:本文档由 Laravel China 社区 [laravel-china.org] 组织翻译。

文档永久地址: http://d.laravel-china.org