用户授权
用户授权
用户授权
简介
除了提供开箱即用的 用户认证服务外,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.image
和posts.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 在授权动作访问指定模型的时候使用哪种策略:
PostPolicy::class, ]; /** * 注册任意应用认证、应用授权服务 * * @return void */ public function boot() { $this->registerPolicies(); // } }
策略自动发现
只要模型和策略遵循标准的 Laravel 命名约定,Laravel 就可以自动发现策略,而不是手动注册模型策略。具体而言,策略必须位于包含模型的目录下的Policies
目录中。因此,例如模型可以放在app
目录中,而策略可以放在app/Policies
目录中。此外,策略名称必须与模型名称匹配,并具有Policy
后缀。因此,User
模型将对应于UserPolicy
类。
如果您想提供自己的策略发现逻辑,可以使用Gate :: guessPolicyNamesUsing
方法注册自定义回调。通常,应该从应用程序的AuthServiceProvider
的boot
方法调用此方法:
use Illuminate\Support\Facades\Gate; Gate::guessPolicyNamesUsing(function ($modelClass) { // return policy class name... });
{note} 在
AuthServiceProvider
中显式映射的任何策略都将优先于自动发现策略。
编写策略
策略方法
一旦授权策略被注册,你就可以为授权过后的每个动作添加方法。比如,我们在PostPolicy
中定义一个update
方法,它会判断指定的User
是否可以更新指定的Post
实例。
update
方法接收User
和Post
实例作为参数,并且应该返回true
或者false
来表明用户是否被授权更新指定的Post
。所以在这个例子中,我们需要判断用户的id
是否和 post 中的user_id
匹配。
id === $post->user_id; } }
你可以继续为这个授权策略定义额外的方法。比如,你可以定义view
或者delete
方法来授权Post
的多种行为,还可以为自定义的策略方法起一个你自己喜欢的名字。
{tip} 如果在 Artisan 控制台生成策略时,使用
--model
选项,它会包含进去view
,create
,update
和delete
动作方法。
不包含模型方法
一些策略方法只接收当前认证通过的用户作为参数,而不用传入与授权相关的模型实例。最常见的应用场景就是授权create
动作。比如,如果你正在创建一篇博客,你可能希望先检查一下当前用户是否有权限创建它。
当定义一个不需要传入模型实例的策略方法时,比如create
方法,它就是不接收模型实例作为参数。你应该定义这个方法只接收授权过的用户作为参数。
/** * 判断用户是否可以创建请求。 * * @param \App\User $user * @return bool */ public function create(User $user){ // }
访客用户
默认情况下,如果传入的 HTTP 请求不是经过身份验证的用户发起的,那么所有的 gates 和策略都会自动返回false
。 然而,你可以允许这些授权检查通过声明一个『可选的』类型提示或为用户参数定义提供null
默认值,从而传递到你的 gates 和策略中:
id === $post->user_id; } }
策略过滤器
对特定用户,你可能希望通过指定的策略授权所有动作。 要达到这个目的,可以在策略中定义一个before
方法。before
方法会在策略中其它所有方法之前执行,这样提供了一种方式来授权动作而不是指定的策略方法来执行判断。这个功能最常见的场景是授权应用的管理员可以访问所有动作:
public function before($user, $ability){ if ($user->isSuperAdmin()) { return true; } }
如果你想拒绝某个用户所有的授权,你应当在before
方法中返回false
。如果返回值是null
,那么授权会在这个策略中失败。
{note} 策略类的
before
方法不会被调用,如果该类不包含与被检查的功能名称相符的方法。
使用策略授权动作
通过用户模型
Laravel 内置的User
模型包含两个有用的方法来授权动作:can
和cant
。这个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 响应。
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 的路由 / 请求参数的名称作为其第二个参数:
authorizeResource(Post::class, 'post'); } }
{提示} 你可以使用带有
--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
不依赖模型的动作
与大多数其他授权方法一样,如果动作不需要模型实例,则可以将类名传递给@ can
和@ cannot
指令:
@can('create', App\Post::class) @endcan @cannot('create', App\Post::class) @endcannot