Whip Monstrous Code Into Shape - 07 Consider Policies


If you find that your controllers are performing too much authorization logic, you might consider policy objects. This allows you to assign a name to important (possibly complex) authorization logic that can reused anywhere. Or, in other words, if you make five different verifications to determine if a member may be added to a team, why not instead reference an AddTeamMemberPolicy object? Let's review two different ways to accomplish this, first using a simple class, and then leveraging Laravel's built-in policy functionality.

这节视频讲的是在控制器中使用 policy(策略),策略是在特定模型或者资源中组织授权逻辑的类,通常用的比较多的是在一些需要登录认证或者权限认证的控制器中,但是也别局限在这个方面。

我们先用自己定义的 policy 类来实现,接着用 Laravel policy 来实现。

假设有个这样的场景:添加一个成员到一个小组中,添加之前需要登录认证,接着验证小组的所有者才能添加,最后需要验证小组没有满员才能添加,控制器的代码如下:

public function store(Team $team)
{
    // 如果没有登录,无法添加
    if (auth()->guest()) {
        abort(403, '未登录.');
    }

    // 如果不是所有者,无法添加
    if ($team->owner_id != auth()->user()->id) {
        abort(403, '不是小组所有者,无法添加.');
    }

    // 如果小组成员已满,无法添加
    if ($team->isMaxedOut()) {
        abort(403, '小组满员,无法添加.');
    }

    return 'add the user to the team';
}

通常在控制器中如果出现了比较多的 if 判断,这个时候就要考虑如何重构了。我们自定义 policy 听起来复杂,实际上很简单,就是把那几个逻辑判断添加到一个单独的 policy 类中。当不满足条件时丢出了个 exception 或 abort 中断继续执行就行。

这个时候可以创建一个 AddTeamMemberPolicy ,把几个逻辑判断放到里面去,然后在控制器中实例化这个类并调用验证方法。

class AddTeamMemberPolicy
{
    /**
     * @var Team
     */
    private $team;

    public function __construct(Team $team)
    {

        $this->team = $team;
    }

    public function allows()
    {
        // 如果没有登录,无法添加
        if (auth()->guest()) {
            abort(403, '未登录.');
        }

        // 如果不是所有者,无法添加
        if ($this->team->owner_id != auth()->user()->id) {
            abort(403, '不是小组所有者,无法添加.');
        }

        // 如果小组成员已满,无法添加
        if ($this->team->isMaxedOut()) {
            abort(403, '小组满员,无法添加.');
        }
    }
}

控制器中实例化这个类并调用 allows 方法,重构后的控制器简洁了不少。

public function store(Team $team)
{
    (new AddTeamMemberPolicy($team))->allows();

    return 'add the user to the team';
}

判断是不是小组成员的这个逻辑也可以重构到 Team 模型中,看起来更合理一些:

if (! $this->team->isOwner()) {
        abort(403, '不是小组所有者,无法添加.');
    }

// Team 模型中添加方法:
public function isOwner(){
    return $this->owner_id == auth()->user()->id
}

上面就是我们自己建的简单的 policy 类,接下来看看 Laravel 中的 Policy。在上面的例子中 AddTeamMemberPolicy 跟 Model 没有多大的联系,但是在 laravel 中 Policy 是和模型紧密联系在一起的,虽然比较多的在控制器中使用,但是, Policy 是在特定模型或者资源中组织授权逻辑的类,不要搞混了。

首先通过命令行创建一个 TeamPolicy,php artisan make:policy TeamPolicy;

class TeamPolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }
}

创建了之后需要在 AuthServiceProvider 中注册绑定,从绑定也可以看出来,Policy 是跟 Model 关联的。

protected $policies = [
    'App\Team' => 'App\Policies\TeamPolicy'
];

这个时候我们同样的把判断逻辑放在 TeamPolicy中。

public function create(User $user, Team $team)
{
    // 如果没有登录,无法添加
    if (auth()->guest()) {
        abort(403, '未登录.');
    }

    // 如果不是所有者,无法添加
    if (! $team->isOwner()) {
        abort(403, '不是小组所有者,无法添加.');
    }

    // 如果小组成员已满,无法添加
    if ($team->isMaxedOut()) {
        abort(403, '小组满员,无法添加.');
    }

    // 一定要返回 true,只有返回 true 程序才继续运行
    return true;
}

在控制器中我们可以直接用 $this->authorize($team); ,要注意几点的是:

  • 控制器的 authorize 方法定义在 AuthorizesRequests 这个 trait 中,确保使用了这个 trait,如果继承自 App\Http\Controllers\Controller,已经使用了这个 trait;

  • 这个方法并不是直接调用 TeamPolicy 的方法,而是通过 Illuminate\Auth\Access\Gate 这个类的 authorize 方法来调用 TeamPolicy;

  • 我们在 TeamPolicy 里面定义的 create 方法,第一个 user 参数代表的是当前登录的用户,在 Gate 中会自动获取,无需传入;

  • TeamPolicy 里面定义的 create 方法跟 Controller 没有任何关系,毋宁说create跟 User 能够执行的动作有关。比如 User 模型有个 can 方法,$user->can('create', $team),这个 store 就是 TeamPolicy 里面定义的验证方法;为什么控制中会自动调用这个方法呢?实际上控制器的 authorize 方法是可以接收两个参数的,第一个就是我们在 policy 里面定义的方法名,如果没有传入的话,AuthorizesRequests 会获取控制器里面调用的方法然后做一个映射,基本上就是 resource 控制器的几个动作的映射,通过 debug_backtrace 函数可以追踪到调用 authorize 方法的控制器方法,在例子中控制器方法对应的是 store,所以映射成 Policy 里面的 create 方法;

[
    'show' => 'view',
    'create' => 'create',
    'store' => 'create',
    'edit' => 'update',
    'update' => 'update',
    'destroy' => 'delete',
];
  • 从上一点可以知道,如果不是非常标准的 resource 风格控制器,最好还是显式指明验证的动作:$this->authorize('create', $team);

接下来优化 TeamPolicy,首先第一点就是没有必要验证是否登录了,因为使用 Gate 的 authorize 方法第一步就会验证用户是否登录,所以可以去掉;

public function create(User $user, Team $team)
{
    // 如果不是所有者,无法添加
    if (! $team->isOwner()) {
        abort(403, '不是小组所有者,无法添加.');
    }

    // 如果小组成员已满,无法添加
    if ($team->isMaxedOut()) {
        abort(403, '小组满员,无法添加.');
    }
}

如果需要在验证前判断某个条件,如果符合则跳过接下来的验证,这个时候可以在 Policy 中添加 before 方法,Gate 在验证之前会调用,如果返回 true 则跳过接下来的验证。

public function before($user)
{
    if ($user->isAdmin())
    {
        return true;
    }
}

为了避免在所有的 policy 中都添加这个方法,你可以把 before 添加到 Gate 中,在 ServiceProvider 的 boot 方法中调用 Gate 的 before 方法注册一个闭包:

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

在文档中 Gate 和 Policy 的用法写的很清楚,原理也很简单,有空可以看看 Illuminate\Auth\Access\Gate 的源码,感觉权限验证用 Gate + Policy 足够了。

本帖已被设为精华帖!
本帖由系统于 7年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!