背景:
帖子属于某个人(拥有属性user_id),如果这是个私密帖子,只有自己才可以看到,传统的做法是
class PostsController extends Controller
{
public function show($id)
{
$post = Post::findOrFail($id);
//只有登录的用户id和帖子的所属user_id相同才可以通过
if (auth()->id() != $post->user_id) {
// 403 权限错误
abort(403, 'Sorry, not sorrry.');
}
return $post->title;
}
}
以上做法问题不大,但是有没有更优雅的做法呢,今天要讲的就是laravel的用户授权,Laravel 有 2 种主要方式来实现用户授权:gates 和策略。
1. Gates
1.1 一个简单的使用Gates的例子
- 准备工作:
创建迁移文件
php artisan make:migration create_post_table --create=posts
文件内容
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned()->index();
$table->string('title');
$table->text('body');
$table->timestamps();
});
DB::table('posts')->insert(
array(
'user_id' => 1,
'title' => '学习用户授权',
'body' => '两种方式,Gates、策略',
)
);
开始迁移
php artisan migration
我们主要使用user表(laravel自带迁移文件)和posts表,其它创建Model等工作自行完成
- 注册Gates,在服务提供器中注册
服务提供器 AppProvidersAuthServiceProvider.php
/**
* 注册任意用户认证、用户授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', function ($user, $post) {
return $user->id == $post->user_id;
});
}
- 授权判定,可以在控制器中进行权限的判定
控制器 app/Http/Controllers/PostManager.php
public function show($id)
{
$post = Post::findOrFail($id);
if (Gate::allows('update-post', $post)) {
// 指定用户可以更新博客...
}
abort(403, 'Sorry, Permission denied');
}
以上就是一个简单的用户授权,是不是很简单,下面我们理解下更多用法
1.2 编写Gates
典型的做法是在 AppProvidersAuthServiceProvider 类中使用 Gate facade 定义。Gates 接受一个用户实例作为第一个参数,并且可以接受可选参数。
- 匿名函数
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', function ($user, $post) {
return $user->id == $post->user_id;
});
}
- 还可以使用Class@method风格字符串,比如控制器
Gate::define('update-post', 'PostPolicy@update');
- 使用资源Gates
一次性定义多个Gates
Gate::resource('posts', 'PostPolicy');
等同于定义了下面4个功能
Gate::define('posts.view', 'PostPolicy@view');
Gate::define('posts.create', 'PostPolicy@create');
Gate::define('posts.update', 'PostPolicy@update');
Gate::define('posts.delete', 'PostPolicy@delete');
还可以传递第三个参数给resource方法,以增加功能
Gate::resource('posts', 'PostPolicy', [
'image' => 'updateImage',
'photo' => 'updatePhoto',
]);
1.3 授权动作
注意: 上面我们定义Gate的时候,闭包函数第一个参数为$user, 但是你并不需要传递当前认证通过的用户给这些方法。Laravel 会自动处理好传入的用户,然后传递给 gate 闭包函数
- 基本用法,使用Gate Facades
//允许授权
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)) {
// 指定用户不能更新博客...
}
2. policy策略
显然Gates简单易用,但是当我们需要授权的动作过多的时候,就显得比较臃肿了,管理起来麻烦,laravel提供了我们另一种方式,来实现同样的功能,就是policy
2.1 还是先看个例子
- 创建策略文件
php artisan make:policy PostPolicy --model=Post
- 编写文件
以上命令会在appPoliciesPostPolicy.php文件,该文件已经包含了4个基本的「CRUD」策略方法,我们可以增删各种方法,这里只补充view方法
public function view(User $user, Post $post)
{
//
return $user->id == $post->user_id;
}
public function create(User $user)
{
//
}
public function update(User $user, Post $post)
{
//
}
public function delete(User $user, Post $post)
{
//
}
- 注册策略
服务提供器 AppProvidersAuthServiceProvider.php, 更改$policies属性
protected $policies = [
'AppPost' => 'AppPoliciesPostPolicy',
];
- 授权判定
控制器 app/Http/Controllers/PostManager.php
public function show($id)
{
$post = Post::findOrFail($id);
if ($user->can('views', $post)) {
// 指定用户可以 ......
}
abort(403, 'Sorry, Permission denied');
}
以上就是使用policy的一个简单实例,下面还是进行更细致的梳理
2.2 编写策略
生成策略文件后,我们可以自己创建删除方法,以满足我们的需求,可以为自定义策略方法使用自己喜欢的名字
public function view(User $user, Post $post)
{
//
return $user->id == $post->user_id;
}
public function create(User $user)
{
//
}
// 其它更多需要的方法
我们还可以使用策略过滤器
比如我们想要超级管理员有所有权限,可以在策略中定义一个 before 方法
public function before($user, $ability)
{
if ($user->isSuperAdmin()) {
return true;
}
}
如果你想拒绝用户所有的授权,你应该在 before 方法中返回 false。如果返回的是 null,则通过其它的策略方法来决定授权与否。
2.3 授权策略
2.3.1 通过用户模型
- 指定模型的动作
Laravel 应用内置的 User 模型包含 2 个有用的方法来授权动作:can 和 cant
if ($user->can('update', $post)) {
//
}
if ($user->cant('update', $post)) {
//
}
注意: 如果指定模型的 策略已被注册,can 方法会自动调用核实的策略方法并且返回 boolean 值。如果没有策略注册到这个模型,can 方法会尝试调用和动作名相匹配的基于闭包的 Gate。
- 不指定模型的动作
比较下面的两个方法
public function view(User $user, Post $post)
{
//
return $user->id == $post->user_id;
}
public function create(User $user)
{
//
}
试想,如果我们有另个一个VideoPolice, 同样有view和create方法。
public function view(User $user, Video $video)
{
//
return $user->id == $video->user_id;
}
public function create(User $user)
{
//
}
不难想象,尽管第一参数都是view, 第二个传入了模型实例,我们在注册策略的时候做了模型和策略的映射,这样就可以区分使用的是PostPolicy还是VideoPolicy
$user->can('view', $post);
$user->can('view', $video);
那么create方法呢, 我们可以传递一个类名给 can 方法。当授权动作时,这个类名将被用来判断使用哪个策略
$user->can('create', Post::class);
$user->can('view', Video::class);
2.3.2 通过中间件
- 通过隐式模型绑定,指定模型动作
use AppPost;
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新博客...
})->middleware('can:update,post');
关于隐式模型绑定,这里顺便提一下,更详细的请自己查询。
首先可以定义这样一条路由
Route::get('/test/{post}','TestController@test');
控制器引入模型文件
use AppHttpModelPost;
test方法传入实例,并类型提示
public function test(Post $post)
{
http_response_code(500);
dd($post);
}
我们访问这样的一条路由 http://xxxx.com/test/1 ,将dd()出id=1 的post实例,我们并没有专门的实例化post这个model,但是我们自动返回了id=参数的post实例
- 不需要指定模型的动作
Route::post('/post', function () {
// 当前用户可以创建博客...
})->middleware('can:create,AppPost');
2.3.3 通过控制器辅助函数autorize()
如果动作不被授权,authorize 方法会抛出 IlluminateAuthAccessAuthorizationException 异常,然后被 Laravel 默认的异常处理器转化为带有 403 状态码的 HTTP 响应:
- 指定模型动作
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
// 当前用户可以更新博客...
}
- 不指定模型动作
public function create(Request $request)
{
$this->authorize('create', Post::class);
// 当前用户可以新建博客...
}
2.3.4 通过blade模板
可以使用@can 和 @cannot
- 指定模型动作
@can('update', $post)
<!-- 当前用户可以更新博客 -->
@elsecan('create', $post)
<!-- 当前用户可以新建博客 -->
@endcan
@cannot('update', $post)
<!-- 当前用户不可以更新博客 -->
@elsecannot('create', $post)
<!-- 当前用户不可以新建博客 -->
@endcannot
- 不指定模型动作
@can('create', AppPost::class)
<!-- 当前用户可以新建博客 -->
@endcan
@cannot('create', AppPost::class)
<!-- 当前用户不可以新建博客 -->
@endcannot
实际上@can 和@cannot 提供了方便的缩写, 分别等同于下面写法
@if (Auth::user()->can('update', $post))
<!-- 当前用户可以更新博客 -->
@endif
@unless (Auth::user()->can('update', $post))
<!-- 当前用户不可以更新博客 -->
@endunless