# 用户授权
除了提供开箱即用的[用户认证](https://laravel-china.org/docs/laravel/5.7/authentication)服务外,Laravel还提供了一种简单的方法来处理用户的授权动作。与用户认证一样,Laravel的授权方法很简单,授权操作有两种主要方式:gates和策略。
可以把 gates 和策略比作路由和控制器。Gates提供了一种简单的基于闭包的授权方法,而策略和控制器类似,围绕特定模型或资源对其逻辑进行分组来实现授权认证。我们先探索gates,然后检查政策。
在构建一个应用的时候,不用在专门使用 gates 或者只使用策略之间进行选择。大部分应用很可能同时包含 gates 和策略, 并且能够很好的进行工作。 Gates 大部分应用在模型和资源没有关系的地方,比如查看管理员的面板。与之相反,策略应该在特定的模型或者资源中使用。
## Gates
### 编写 Gates
Gates 是用来决定用户是否授权执行给予动作的一个闭包函数,并且典型的做法就是在
`App\Providers\AuthServiceProvider`中使用`Gate`来定义. Gates 总是接收一个用户实例作为第一个参数,并且可以接收可选参数,比如相关的 Eloquent 模型:
~~~php
/**
* 注册任意用户认证、用户授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', function ($user, $post) {
return $user->id == $post->user_id;
});
}
~~~
Gates 也可以使用类似控制器方法`Class@method`风格的回调字符串来定义:
~~~php
/**
* 注册任意用户认证、用户授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', 'App\Policies\PostPolicy@update');
}
~~~
#### 资源 Gates
你还可以使用`resource`方法去一次性的定义多个 Gate 方法:
~~~php
Gate::resource('posts', 'App\Policies\PostPolicy');
~~~
上面的手动定义和以下的 Gate 定义效果是相同的:
~~~php
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');
~~~
默认情况下将会定义`view`,`create`,`update`, 和`delete`方法. 通过将一个数组作为第三个参数传给`resource`方法. 你可以覆盖或者添加到默认的方法中。数组的键定义能力的名称,值定义方法的名称。例如,下面的代码将创建两个新的 Gate 定义 -`posts.image`和`posts.photo`:
~~~php
Gate::resource('posts', 'PostPolicy', [
'image' => 'updateImage',
'photo' => 'updatePhoto',
]);
~~~
### 授权动作
使用 gates 来授权动作的时候, 你应该使用`allows`或者`denies`方法。 注意,你并不需要给已经认证通过的用户传递这些方法。 Laravel 会自动处理好已经认证通过的用户,然后传递给 gete 闭包函数:
~~~php
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)) {
// 用户不能更新...
}
~~~
#### Gate 拦截检查
有时,你可能希望将所有能力授予特定用户。所以你可以在所有其他授权检查之前使用`before`方法来定义运行的回调:
~~~php
Gate::before(function ($user, $ability) {
if ($user->isSuperAdmin()) {
return true;
}
});
~~~
如果`before`回调方法返回的是非null的结果,则结果将被视为检查结果。
在每次授权检查后你可以使用`after`方法定义要执行的回调。 但是,你不能从`after`回调方法中修改授权检查的结果:
~~~php
Gate::after(function ($user, $ability, $result, $arguments) {
//
});
~~~
## 创建策略
### 生成策略
策略是在特定模型或者资源中组织授权逻辑的类。例如,你的应用是一个博客,那么你在创建或者更新博客的时候,你可能会有一个`Post`模型和一个对应的`PostPolicy`来授权用户动作。
你可以使用`artisan 命令`[artisan command](https://laravel-china.org/docs/laravel/5.7/artisan)中的`make:policy`[artisan command](https://laravel-china.org/docs/laravel/5.7/artisan)命令来生成策略。 生成的策略将放置在`app/Policies`目录中. 如果在你的应用中不存在这个目录,那么 Laravel 将会为你自动生成:
~~~php
php artisan make:policy PostPolicy
~~~
`make:policy`命令会生成一个空的策略类。如果你想生成的类包含基本的 「CRUD」策略方法,你可以在执行命令的时候指定`--model`这个选项:
~~~php
php artisan make:policy PostPolicy --model=Post
~~~
> {tip} 所有的策略会通过 Laravel 的[服务容器](https://laravel-china.org/docs/laravel/5.7/container),来解析,允许你在策略构造器中对任何需要的依赖使用类型提示,并且自动注入。
### 注册策略
一旦策略存在,它就需要进行注册。新的 Laravel 应用中包含的`AuthServiceProvider`有一个`policies`属性,可以将各种模型对应到它们的策略中。注册一个策略将引导 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();
//
}
}
~~~
## 编写策略
### 策略方法
一旦授权策略被注册,你就可以为授权过后的每个动作添加方法。比如,我们在`PostPolicy`中定义一个`update`方法,它会判断指定的`User`是否可以更新指定的`Post`实例。
`update`方法接收`User`和`Post`实例作为参数,并且应该返回`true`或者`false`来表明用户是否被授权更新指定的`Post`。所以在这个例子中,我们需要判断用户的`id`是否和 post 中的`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;
}
}
~~~
你可以继续为这个授权策略定义额外的方法。比如,你可以定义`view`或者`delete`方法来授权`Post`的多种行为,还可以为自定义的策略方法起一个你自己喜欢的名字。
> {tip} 如果在 Artisan 控制台生成策略时,使用`--model`选项,它会包含进去`view`,`create`,`update`和`delete`动作方法。
### 不包含模型方法
一些策略方法只接收当前认证通过的用户作为参数,而不用传入与授权相关的模型实例。最常见的应用场景就是授权`create`动作。比如,如果你正在创建一篇博客,你可能希望先检查一下当前用户是否有权限创建它。
当定义一个不需要传入模型实例的策略方法时,比如`create`方法,它就是不接收模型实例作为参数。你应该定义这个方法只接收授权过的用户作为参数。
~~~php
/**
* 判断用户是否可以创建请求。
*
* @param \App\User $user
* @return bool
*/
public function create(User $user)
{
//
}
~~~
### 访客用户
默认情况下,如果传入的 HTTP 请求不是经过身份验证的用户发起的,那么所有的 gates 和策略都会自动返回`false`。 然而,你可以允许这些授权检查通过声明一个『可选的』类型提示或为用户参数定义提供`null`默认值,从而传递到你的 gates 和策略中:
~~~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`,那么授权会在这个策略中失败。
> {note} 策略类的`before`方法不会被调用,如果该类不包含与被检查的功能名称相符的方法。
## 使用策略授权动作
### 通过用户模型
Laravel 内置的`User`模型包含两个有用的方法来授权动作:`can`和`cant`。这个`can`方法需要指定授权的动作以及相关的模型。例如,判断是否授权一个用户更新指定的`Post`模型:
~~~php
if ($user->can('update', $post)) {
//
}
~~~
如果指定模型的 「[策略已被注册](https://laravel-china.org/docs/laravel/5.7/authorization/2271#registering-policies)」,`can`方法会自动调用合适的策略并返回一个 boolean 值。如果没有策略注册到这个模型,`can`方法会尝试调用和指定动作名称相匹配的基于闭包的 Gate。
#### 不需要指定模型的动作
记住,一些动作,比如`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`中间件两个参数。第一个参数是需要授权的动作名称,第二个参数是我们希望传递给策略方法的路由参数。在这种情况下,我们使用了「[隐式路由绑定](https://laravel-china.org/docs/laravel/5.7/routing#implicit-binding)」,一个`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);
// 当前用户可以新建博客...
}
~~~
### 通过 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
~~~
#### 不需要指定模型的动作
和大部分其它的授权方法相似,当动作不需要模型实例时,你可以传递一个类名给`@can`和`@cannot`指令:
~~~php
@can('create', App\Post::class)
<!-- 当前用户可以进行创建操作 -->
@endcan
@cannot('create', App\Post::class)
<!-- 当前用户不能进行创建操作 -->
@endcannot
~~~