用户授权

简介

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

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

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

Gates

编写 Gates

Gates 是用来决定用户是否授权执行给予动作的一个闭包函数,并且典型的做法就是在 App\Providers\AuthServiceProvider 中使用 Gate 来定义. Gates 总是接收一个用户实例作为第一个参数,并且可以接收可选参数,比如相关的 Eloquent 模型:

  1. /**
  2. * 注册任意用户认证、用户授权服务。
  3. *
  4. * @return void
  5. */
  6. public function boot()
  7. {
  8. $this->registerPolicies();
  9. Gate::define('edit-settings', function ($user) {
  10. return $user->isAdmin;
  11. });
  12. Gate::define('update-post', function ($user, $post) {
  13. return $user->id === $post->user_id;
  14. });
  15. }

Gates 也可以使用类似控制器方法 Class@method 风格的回调字符串来定义:

  1. /**
  2. * 注册任意用户认证、用户授权服务。
  3. *
  4. * @return void
  5. */
  6. public function boot()
  7. {
  8. $this->registerPolicies();
  9. Gate::define('update-post', 'App\Policies\PostPolicy@update');
  10. }

授权动作

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

  1. if (Gate::allows('edit-settings')) {
  2. // 指定当前用户可以编辑设置
  3. }
  4. if (Gate::allows('update-post', $post)) {
  5. // 指定当前用户可以进行更新...
  6. }
  7. if (Gate::denies('update-post', $post)) {
  8. // 指定当前用户不能更新...
  9. }

如果你想判断一个特定的用户是否已经被授权访问某个动作, 你可以使用在 Gate 在facade的 forUser 方法:

  1. if (Gate::forUser($user)->allows('update-post', $post)) {
  2. // 用户可以更新...
  3. }
  4. if (Gate::forUser($user)->denies('update-post', $post)) {
  5. // 用户不能更新...
  6. }

您可以使用 anynone 方法一次授权多个操作:

  1. if (Gate::any(['update-post', 'delete-post'], $post)) {
  2. // 用户可以更新或删除
  3. }
  4. if (Gate::none(['update-post', 'delete-post'], $post)) {
  5. // 用户不能更新或删除
  6. }

授权或抛出异常

如果要尝试对某个操作进行授权,并在未授权用户进行该操作的情况下抛出 illuminate\auth\access\authorizationexception,则可以使用 gate::authorize方法。authorizationexception 的实例将自动转换为 403 http 响应:

  1. Gate::authorize('update-post', $post);
  2. // 当前行为已授权...

提供上下文

能够用于授权的 Gate 方法(allowsdeniescheckanynoneauthorizecancannot)和授权[blade directives](@can@cannot@canany)可以接收一个数组作为第二个参数。这些数组元素作为参数传递给 gate ,在做出授权决策时可用于其他上下文:

  1. Gate::define('create-post', function ($user, $category, $extraFlag) {
  2. return $category->group > 3 && $extraFlag === true;
  3. });
  4. if (Gate::check('create-post', [$category, $extraFlag])) {
  5. // The user can create the post...
  6. }

Gate 响应

到目前为止,我们只检查了返回简单布尔值的 Gate 。但是,有时你可能希望返回更详细的响应,包括错误消息。为此,你可以从你的 Gate 返回illuminate\auth\access\response

  1. use Illuminate\Auth\Access\Response;
  2. use Illuminate\Support\Facades\Gate;
  3. Gate::define('edit-settings', function ($user) {
  4. return $user->isAdmin
  5. ? Response::allow()
  6. : Response::deny('You must be a super administrator.');
  7. });

从 Gate 返回授权响应时,gate::allows 方法仍将返回一个简单的布尔值。但是可以使用 gate::inspect 方法获取 Gate 返回的完整授权响应:

  1. $response = Gate::inspect('edit-settings', $post);
  2. if ($response->allowed()) {
  3. // 当前行为已授权...
  4. } else {
  5. echo $response->message();
  6. }

当然,当使用 gate::authorize 方法抛出 authorizationexception 时,如果操作未经授权,则授权响应提供的错误消息将传播到 http 响应:

  1. Gate::authorize('edit-settings', $post);
  2. // 当前行为已授权...

Gate 拦截检查

有时,你可能希望将所有能力授予特定用户。所以你可以在所有其他授权检查之前使用 before 方法来定义运行的回调:

  1. Gate::before(function ($user, $ability) {
  2. if ($user->isSuperAdmin()) {
  3. return true;
  4. }
  5. });

如果 before 回调方法返回的是非 null 的结果,则结果将被视为检查结果。

在每次授权检查后你可以使用 after 方法定义要执行的回调。 但是,你不能从 after 回调方法中修改授权检查的结果:

  1. Gate::after(function ($user, $ability, $result, $arguments) {
  2. if ($user->isSuperAdmin()) {
  3. return true;
  4. }
  5. });

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

创建策略

生成策略

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

可以使用 make:policy artisan command 生成策略。 生成的策略将放在 app/Policies 目录。如果您的应用程序中不存在此目录,Laravel 将为您创建它:

  1. php artisan make:policy PostPolicy

make:policy 命令将生成一个空策略类。如果你想生成一个包含基本的 "CRUD" 策略方法的类,你可以在执行命令时指定一个 —model

  1. php artisan make:policy PostPolicy --model=Post

建议:所有策略都通过 Laravel 解析 service container,允许您在策略的构造函数中键入提示任何需要的依赖项,以便自动注入它们。

注册策略

一旦策略存在,就需要注册它。新 Laravel 应用程序中包含的 AuthServiceProvider 包含一个 policy 属性,它将您的 Eloquent 模型映射到它们相应的策略。注册一个策略将指导 Laravel 在授权针对给定模型的操作时使用哪个策略:

  1. <?php
  2. namespace App\Providers;
  3. use App\Policies\PostPolicy;
  4. use App\Post;
  5. use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
  6. use Illuminate\Support\Facades\Gate;
  7. class AuthServiceProvider extends ServiceProvider
  8. {
  9. /**
  10. * 应用程序的策略映射。
  11. *
  12. * @var array
  13. */
  14. protected $policies = [
  15. Post::class => PostPolicy::class,
  16. ];
  17. /**
  18. * 注册任何应用程序 authentication / authorization 服务。
  19. *
  20. * @return void
  21. */
  22. public function boot()
  23. {
  24. $this->registerPolicies();
  25. //
  26. }
  27. }

策略自动发现

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

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

  1. use Illuminate\Support\Facades\Gate;
  2. Gate::guessPolicyNamesUsing(function ($modelClass) {
  3. // 返回策略类名…
  4. });

注意:在 AuthServiceProvider 中显式映射的任何策略都将优先于任何潜在的自动发现策略。

编写策略

策略方法

一旦注册了策略,您可以为它授权的每个操作添加方法。例如,让我们在 PostPolicy 上定义一个 update 方法,它确定给定的 User 是否可以更新给定的 Post 实例。

update 方法将接收一个 User 和一个 Post 实例作为参数,并应返回 truefalse,指示用户是否被授权更新给定的 Post。所以,在这个例子中,让我们验证用户的 id 是否与帖子上的 user_id 匹配:

  1. <?php
  2. namespace App\Policies;
  3. use App\Post;
  4. use App\User;
  5. class PostPolicy
  6. {
  7. /**
  8. * 确定用户是否可以更新给定的帖子。
  9. *
  10. * @param \App\User $user
  11. * @param \App\Post $post
  12. * @return bool
  13. */
  14. public function update(User $user, Post $post)
  15. {
  16. return $user->id === $post->user_id;
  17. }
  18. }

您可以根据策略授权的各种操作的需要,继续在策略上定义其他方法。例如,您可以定义 viewdelete 方法来授权各种 Post 操作,但请记住,您可以自由地为策略方法指定任何名称。

建议: 如果您在通过 Artisan 控制台生成策略时使用了 —model 选项,那么它已经包含了 viewAnyviewcreateupdatedeleterestoreforceDelete 操作的方法。

策略响应

到目前为止,我们只研究了返回简单布尔值的策略方法。然而,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从您的策略方法返回一个\Auth\Access\Response

  1. use Illuminate\Auth\Access\Response;
  2. /**
  3. * 确定用户是否可以更新给定的帖子。
  4. *
  5. * @param \App\User $user
  6. * @param \App\Post $post
  7. * @return \Illuminate\Auth\Access\Response
  8. */
  9. public function update(User $user, Post $post)
  10. {
  11. return $user->id === $post->user_id
  12. ? Response::allow()
  13. : Response::deny('You do not own this post.');
  14. }

当从策略返回授权响应时,Gate::allows 方法仍然返回一个简单的布尔值;但是,你可以使用 Gate::inspect 方法来获得Gate返回的完整授权响应:

  1. $response = Gate::inspect('update', $post);
  2. if ($response->allowed()) {
  3. // 该动作授权通过...
  4. } else {
  5. echo $response->message();
  6. }

当然,当使用 Gate::authorize 方法在未授权操作时抛出 AuthorizationException,授权响应提供的错误消息将传播到 HTTP 响应:

  1. Gate::authorize('update', $post);
  2. // 该动作授权通过…

不包含模型方法

一些策略方法只接收当前经过身份验证的用户,而不接收它们授权的模型的实例。这种情况在授权 create 操作时最为常见。例如,如果您正在创建博客,您可能希望检查用户是否被授权创建任何帖子。

当定义不接收模型实例的策略方法时,例如 create 方法,它将不接收模型实例。相反,您应该将方法定义为只期望经过身份验证的用户:

  1. /**
  2. * 确定给定用户是否可以创建帖子。
  3. *
  4. * @param \App\User $user
  5. * @return bool
  6. */
  7. public function create(User $user)
  8. {
  9. //
  10. }

Guest 用户

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

  1. <?php
  2. namespace App\Policies;
  3. use App\Post;
  4. use App\User;
  5. class PostPolicy
  6. {
  7. /**
  8. * 确定用户是否可以更新给定的帖子。
  9. *
  10. * @param \App\User $user
  11. * @param \App\Post $post
  12. * @return bool
  13. */
  14. public function update(?User $user, Post $post)
  15. {
  16. return optional($user)->id === $post->user_id;
  17. }
  18. }

策略过滤器

对于某些用户,您可能希望授权给定策略中的所有操作。为此,在策略上定义一个 before 方法。before 方法将在策略上的任何其他方法之前执行,从而使您有机会在实际调用预期的策略方法之前授权操作。此功能最常用于授权应用程序管理员执行任何操作:

  1. public function before($user, $ability)
  2. {
  3. if ($user->isSuperAdmin()) {
  4. return true;
  5. }
  6. }

如果您想拒绝用户的所有授权,您应该从 before 方法返回 false。如果返回 null,则授权将传递给策略方法。

注意:如果策略类的 before 方法不包含与正在检查的功能名称匹配的名称的方法,则不会调用该方法。

使用策略授权动作

通过用户模型

Laravel 应用程序中包含的 User 模型包括两个用于授权操作的有用方法:cancantcan 方法接收您希望授权的操作和相关模型。例如,让我们来确定一个用户是否被授权更新一个给定的 Post 模型:

  1. if ($user->can('update', $post)) {
  2. //
  3. }

如果为给定模型 注册了策略can 方法将自动调用适当的策略并返回布尔结果。如果没有为模型注册策略,can 方法将尝试调用匹配给定操作名称的基于闭包的 Gate。

不需要指定模型的动作

记住,像 create 这样的操作可能不需要模型实例。在这些情况下,可以将类名传递给 can 方法。类名将用于确定在授权操作时使用哪个策略:

  1. use App\Post;
  2. if ($user->can('create', Post::class)) {
  3. // 在相关策略上执行 "create" 方法…
  4. }

通过中间件

Laravel 包含一个中间件,可以在传入的请求到达路由或控制器之前对操作进行授权。默认情况下,Illuminate\Auth\Middleware\Authorize 中间件被分配给 App\Http\Kernel 类中的 can 键。让我们来探索一个使用 can 中间件授权用户更新博客文章的例子:

  1. use App\Post;
  2. Route::put('/post/{post}', function (Post $post) {
  3. // 当前用户可以更新帖子…
  4. })->middleware('can:update,post');

在本例中,我们传递了 can 中间件两个参数。第一个是希望授权的操作的名称,第二个是希望传递给策略方法的路由参数。在本例中,由于我们使用隐式模型绑定,一个 Post 模型将被传递给策略方法。如果用户没有被授权执行给定的操作,则中间件将生成一个带有 403 状态代码的 HTTP 响应。

不需要指定模型的动作

同样,像 create 这样的一些操作可能不需要模型实例。在这些情况下,可以将类名传递给中间件。类名将用于确定在授权操作时使用哪个策略:

  1. Route::post('/post', function () {
  2. // 当前用户可以创建贴子…
  3. })->middleware('can:create,App\Post');

通过控制器辅助函数

除了为 User 模型提供的有用方法之外,Laravel 还为任何扩展 App\Http\Controllers\Controller 基类的控制器提供了一个有用的 authorize 方法。 与 can 方法一样,此方法接受您要授权的操作的名称和相关模型。 如果操作未被授权,authorize方法将抛出一个 Illuminate\Auth\Access\AuthorizationException ,默认的 Laravel 异常处理程序将转换为具有 403 状态代码的HTTP响应:

  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Controllers\Controller;
  4. use App\Post;
  5. use Illuminate\Http\Request;
  6. class PostController extends Controller
  7. {
  8. /**
  9. * 更新指定博客帖子。
  10. *
  11. * @param Request $request
  12. * @param Post $post
  13. * @return Response
  14. * @throws \Illuminate\Auth\Access\AuthorizationException
  15. */
  16. public function update(Request $request, Post $post)
  17. {
  18. $this->authorize('update', $post);
  19. // 当前用户可以更新博客....
  20. }
  21. }

不需要指定模型的动作

如前所述,像create这样的一些动作可能不需要模型实例。 在这些情况下,您应该将类名传递给authorize方法。 类名将用于确定授权操作时使用的策略:

  1. /**
  2. * 创建一个新的博客
  3. *
  4. * @param Request $request
  5. * @return Response
  6. * @throws \Illuminate\Auth\Access\AuthorizationException
  7. */
  8. public function create(Request $request)
  9. {
  10. $this->authorize('create', Post::class);
  11. // 当前用户可以新建博客...
  12. }

授权资源控制器

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

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

  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Controllers\Controller;
  4. use App\Post;
  5. use Illuminate\Http\Request;
  6. class PostController extends Controller
  7. {
  8. public function __construct()
  9. {
  10. $this->authorizeResource(Post::class, 'post');
  11. }
  12. }

以下控制器方法将映射到其对应的策略方法:

控制器方法策略方法
indexviewAny
showview
createcreate
storecreate
editupdate
updateupdate
destroydelete

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

通过 Blade 模板

当编写 Blade 模板时,可能希望仅在用户被授权执行给定操作时才显示页面的一部分。比如,你可能希望仅在用户可以实际更新帖子时显示博客帖子的更新表单。在这样情况下,你可以使用 @can@cannot 等一系列指令:

@can('update', $post)
    <!-- 当前用户可以更新文章 -->
@elsecan('create', App\Post::class)
    <!-- 当前用户可以新建文章 -->
@endcan

@cannot('update', $post)
    <!-- 当前用户不可以更新文章 -->
@elsecannot('create', App\Post::class)
    <!-- 当前用户不可以新建文章 -->
@endcannot

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

@if (Auth::user()->can('update', $post))
    <!-- 当前用户可以更新文章 -->
@endif

@unless (Auth::user()->can('update', $post))
    <!-- 当前用户不可以更新文章 -->
@endunless

您还可以确定用户是否具有来自给定能力列表的任何授权能力。 要实现这一点,请使用 @ canany 指令:

@canany(['update', 'view', 'delete'], $post)
    // 当前用户可以更新,查看和删除文章
@elsecanany(['create'], \App\Post::class)
    // 当前用户可以创建文章
@endcanany

不依赖模型的动作

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

@can('create', App\Post::class)
    <!-- 当前用户可以创建文章 -->
@endcan

@cannot('create', App\Post::class)
    <!-- 当前用户不可以创建文章 -->
@endcannot

提供附加上下文

使用策略授权操作时,您可以将数组作为第二个参数传递给各种授权函数和辅助方法。 数组中的第一个元素将用于确定应调用哪个策略,而其余数组元素作为参数传递给策略方法,并可在进行授权决策时用于其他上下文。 例如,考虑以下 PostPolicy 方法定义,其中包含一个额外的 $ category 参数:

/**
 * 确定用户是否可以更新给定的帖子。
 *
 * @param  \App\User  $user
 * @param  \App\Post  $post
 * @param  int  $category
 * @return bool
 */
public function update(User $user, Post $post, int $category)
{
    return $user->id === $post->user_id && 
           $category > 3;
}

在尝试确定验证过的用户是否可以更新给定文章时,我们可以像这样调用此策略方法:

/**
 * 更新给定的博客文章.
 *
 * @param  Request  $request
 * @param  Post  $post
 * @return Response
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function update(Request $request, Post $post)
{
    $this->authorize('update', [$post, $request->input('category')]);

    //当前用户可以更新这篇博客文章...
}

提供附加上下文

使用策略授权操作时,您可以将数组作为第二个参数传递给各种授权函数和辅助方法。 数组中的第一个元素将用于确定应调用哪个策略,而其余数组元素作为参数传递给策略方法,并可在进行授权决策时用于其他上下文。 例如,考虑以下 PostPolicy 方法定义,其中包含一个额外的 $ category 参数:

/**
 * 确定用户是否可以更新给定的帖子。
 *
 * @param  \App\User  $user
 * @param  \App\Post  $post
 * @param  int  $category
 * @return bool
 */
public function update(User $user, Post $post, int $category)
{
    return $user->id === $post->user_id && 
           $category > 3;
}

在尝试确定验证过的用户是否可以更新给定文章时,我们可以像这样调用此策略方法:

/**
 * 更新给定的博客文章.
 *
 * @param  Request  $request
 * @param  Post  $post
 * @return Response
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function update(Request $request, Post $post)
{
    $this->authorize('update', [$post, $request->input('category')]);

    //当前用户可以更新这篇博客文章...
}

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接 我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

Laravel China 社区:https://learnku.com/docs/laravel/7.x/authorization/7475