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 足够了。
推荐文章: