用户授权


用户授权

简介

除了提供开箱即用的 用户认证服务外,Laravel 还提供了一种简单的方法来处理用户的授权动作。与用户认证一样,Laravel 的授权方法很简单,授权操作有两种主要方式:gates 和策略。

可以把 gates 和策略比作路由和控制器。Gates 提供了一种简单的基于闭包的授权方法,而策略和控制器类似,围绕特定模型或资源对其逻辑进行分组来实现授权认证。我们先探索 gates,然后研究策略。

在构建一个应用的时候,不用在专门使用 gates 或者只使用策略之间进行选择。大部分应用很可能同时包含 gates 和策略, 并且能够很好的进行工作。 Gates 大部分应用在模型和资源没有关系的地方,比如查看管理员的面板。与之相反,策略应该在特定的模型或者资源中使用。

Gates

编写 Gates

Gates 是用来决定用户是否授权执行给予动作的一个闭包函数,并且典型的做法就是在
App\Providers\AuthServiceProvider 中使用 Gate 来定义.  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');

默认情况下将会定义 view, create, update, 和 delete 方法。通过将一个数组作为第三个参数传给  resource 方法。你可以覆盖或者添加到默认的方法中。数组的键定义能力的名称,值定义方法的名称。例如,下面的代码将创建两个新的 Gate 定义 -  posts.imageposts.photo:

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

授权动作

使用 gates 来授权动作的时候, 你应该使用 allows 或者 denies 方法。 注意,不需要将当前已认证用户传递给这些方法。 Laravel 会自动处理好已经认证通过的用户,然后传递给 gete 闭包函数:

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) { 
   //
});

before 检查类似,如果 after 回调返回非 null 结果,则结果将被视为检查结果。

创建策略

生成策略

策略是在特定模型或者资源中组织授权逻辑的类。例如,你的应用是一个博客,那么你在创建或者更新博客的时候,你可能会有一个 Post 模型和一个对应的 PostPolicy 来授权用户动作。

你可以使用 artisan 命令 artisan command 中的 make:policy artisan command 命令来生成策略。 生成的策略将放置在  app/Policies 目录中。如果在你的应用中不存在这个目录,那么 Laravel 将会为你自动生成:

php artisan make:policy PostPolicy

make:policy 命令会生成一个空的策略类。如果你想生成的类包含基本的 「CRUD」策略方法,你可以在执行命令的时候指定  --model 这个选项:

php artisan make:policy PostPolicy --model=Post

{tip} 所有的策略会通过 Laravel 的  服务容器,来解析,允许你在策略构造器中对任何需要的依赖使用类型提示,并且自动注入。

注册策略

一旦策略存在,它就需要进行注册。新的 Laravel 应用中包含的 AuthServiceProvider 有一个 policies 属性,可以将各种模型对应到它们的策略中。注册一个策略将引导 Laravel 在授权动作访问指定模型的时候使用哪种策略:

<?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();   
            //   
     }
   }

策略自动发现

只要模型和策略遵循标准的 Laravel 命名约定,Laravel 就可以自动发现策略,而不是手动注册模型策略。具体而言,策略必须位于包含模型的目录下的 Policies 目录中。因此,例如模型可以放在 app 目录中,而策略可以放在 app/Policies 目录中。此外,策略名称必须与模型名称匹配,并具有 Policy 后缀。因此,User 模型将对应于 UserPolicy 类。

如果您想提供自己的策略发现逻辑,可以使用 Gate :: guessPolicyNamesUsing 方法注册自定义回调。通常,应该从应用程序的 AuthServiceProviderboot 方法调用此方法:

use Illuminate\Support\Facades\Gate;
Gate::guessPolicyNamesUsing(function ($modelClass) {
    // return policy class name...
 });

{note} 在 AuthServiceProvider 中显式映射的任何策略都将优先于自动发现策略。

编写策略

策略方法

一旦授权策略被注册,你就可以为授权过后的每个动作添加方法。比如,我们在 PostPolicy 中定义一个 update 方法,它会判断指定的 User 是否可以更新指定的 Post 实例。

update 方法接收 UserPost 实例作为参数,并且应该返回 true 或者 false 来表明用户是否被授权更新指定的 Post 。所以在这个例子中,我们需要判断用户的 id 是否和 post 中的 user_id 匹配。

<?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 选项,它会包含进去 viewcreateupdatedelete 动作方法。

不包含模型方法

一些策略方法只接收当前认证通过的用户作为参数,而不用传入与授权相关的模型实例。最常见的应用场景就是授权 create 动作。比如,如果你正在创建一篇博客,你可能希望先检查一下当前用户是否有权限创建它。

当定义一个不需要传入模型实例的策略方法时,比如 create 方法,它就是不接收模型实例作为参数。你应该定义这个方法只接收授权过的用户作为参数。

/**
 * 判断用户是否可以创建请求。
 *
 * @param  \App\User  $user
 * @return bool
 */
 public function create(User $user){ 
    //
 }

访客用户

默认情况下,如果传入的 HTTP 请求不是经过身份验证的用户发起的,那么所有的 gates 和策略都会自动返回 false。 然而,你可以允许这些授权检查通过声明一个『可选的』类型提示或为用户参数定义提供 null 默认值,从而传递到你的 gates 和策略中:

<?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 方法会在策略中其它所有方法之前执行,这样提供了一种方式来授权动作而不是指定的策略方法来执行判断。这个功能最常见的场景是授权应用的管理员可以访问所有动作:

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

如果你想拒绝某个用户所有的授权,你应当在 before 方法中返回 false 。如果返回值是 null ,那么授权会在这个策略中失败。

{note} 策略类的 before 方法不会被调用,如果该类不包含与被检查的功能名称相符的方法。

使用策略授权动作

通过用户模型

Laravel 内置的 User 模型包含两个有用的方法来授权动作: cancant。这个 can 方法需要指定授权的动作以及相关的模型。例如,判断是否授权一个用户更新指定的 Post 模型:

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

如果指定模型的 「策略已被注册 」, can 方法会自动调用合适的策略并返回一个 boolean 值。如果没有策略注册到这个模型,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 响应。

<?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 方法。当授权这个动作时,这个类名将被用来判断使用哪个策略:

/**
 * 创建一个新的博客
 *
 * @param  Request  $request
 * @return Response
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
 public function create(Request $request){ 
    $this->authorize('create', Post::class);  
    // 当前用户可以新建博客...
  }

授权资源控制器

如果你使用的是 资源控制器,那么你就可以在控制器构造方法里使用 authorizeResource 方法。该方法会把合适 can 中间件附加到资源控制器相应的方法中。

authorizeResource 方法接收模板类名作为第一个参数,包含模型 ID 的路由 / 请求参数的名称作为其第二个参数:

<?php
    namespace App\Http\Controllers;
    use App\Post;use Illuminate\Http\Request;
    use App\Http\Controllers\Controller;
    class PostController extends Controller{ 
       public function __construct() 
          {       
            $this->authorizeResource(Post::class, 'post');  
           }
     }

{提示} 你可以使用带有 --model 选项的 make:policy 命令去快速生成基于给定模型的策略类::php artisan make:policy PostPolicy --model=Post

通过 Blade 模板

当编写 Blade 模板时,你可能希望页面的指定部分只展示给授权访问指定动作的用户。比如,你可能希望只展示更新的表单给有权更新博客的用户。在这样情况下,你可以使用 @can@cannot 等一系列指令:

@can('update', $post) 
  <!-- The Current User Can Update The Post -->
@elsecan('create', App\Post::class)  
  <!-- The Current User Can Create New Post -->
@endcan
@cannot('update', $post) 
  <!-- The Current User Can't Update The Post -->
@elsecannot('create', App\Post::class)
  <!-- The Current User Can't Create New Post -->
 @endcannot

这些指令是编写 @if@unless 语句的捷径。 @can@cannot 语句分别转化为以下语句:

@if (Auth::user()->can('update', $post))  
  <!-- The Current User Can Update The Post -->
@endif
@unless (Auth::user()->can('update', $post))   
 <!-- The Current User Can't Update The Post -->
@endunless

不依赖模型的动作

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

@can('create', App\Post::class) 
   <!-- The Current User Can Create Posts -->
@endcan
@cannot('create', App\Post::class)   
      <!-- The Current User Can't Create Posts -->
@endcannot
本文章首发在 LearnKu.com 网站上。