Laravel 授权门概述
发布于 作者 Yazid Hanifi
Laravel Gate 提供了一个优雅的机制来确保用户有权对资源执行操作。
在 5.1 版本之前,开发人员使用 Entrust 或 Sentinel 等 ACL 软件包以及中间件进行授权。这种方法的问题在于,您附加给用户的权限只是标志;它们不编码一些用例的权限的复杂逻辑。我们必须在控制器中编写实际的访问逻辑。
Gate 避免了仅使用这些提到的软件包的一些缺点
观点用例:Gate 不会定义如何实现模型;这取决于您。这使您能够以任何您喜欢的方式编写用例中所有复杂的规范。您甚至可以使用 Laravel Gate 与 ACL 软件包一起使用。定义逻辑(策略):使用 Gate,我们可以将访问逻辑与业务逻辑解耦,这有助于消除控制器中的混乱。
使用示例
在这篇文章中,我们将制作一个玩具帖子应用程序来展示 Gate 如何为您提供自由和解耦。该 Web 应用程序将具有两个用户角色(作者和编辑),并具有以下权限
- 作者可以创建帖子。
- 作者可以更新他们的帖子。
- 编辑可以更新任何帖子。
- 编辑可以发布帖子。
创建一个新的 Laravel 项目
首先,创建一个新的 Laravel 5.4 应用程序。
laravel new blog
如果您没有 Laravel 安装程序,请使用 composer create-project
。
composer create-project --prefer-dist laravel/laravel blog
基本配置
更新 .env
文件,并允许 Laravel 访问您创建的数据库。
...APP_URL=http://localhost:8000...DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=dbnameDB_USERNAME=dbuserDB_PASSWORD=yoursecretdbuserpassword...
数据库
现在,让我们创建一个 Post
模型。使用 -m
和 -c
参数,我们可以为帖子创建迁移和控制器。
php artisan make:model Post -m -c
接下来,更新帖子迁移并添加以下字段。
打开帖子迁移文件,并将以下内容添加到 up
方法中
Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->string('slug')->unique(); $table->text('body'); $table->boolean('published')->default(false); $table->unsignedInteger('user_id'); $table->timestamps(); $table->foreign('user_id')->references('id')->on('users');});
我们需要添加几个其他表:roles
和 user_roles
联动表。我们将像 Sentinel
一样将权限放在角色表中。
php artisan make:model Role -m
Schema::create('roles', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('slug')->unique(); $table->jsonb('permissions')->default('{}'); // jsonb deletes duplicates $table->timestamps();});
最后,role_users
联动表。
php artisan make:migration create_role_users_table
Schema::create('role_users', function (Blueprint $table) { $table->unsignedInteger('user_id'); $table->unsignedInteger('role_id'); $table->timestamps(); $table->unique(['user_id','role_id']); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');}); ...
播种数据库
为了完成我们的数据库初始化,我们将为角色创建种子。
php artisan make:seeder RolesSeeder
use Illuminate\Database\Seeder;use App\Role; class RolesSeeder extends Seeder{ public function run() { $author = Role::create([ 'name' => 'Author', 'slug' => 'author', 'permissions' => [ 'create-post' => true, ] ]); $editor = Role::create([ 'name' => 'Editor', 'slug' => 'editor', 'permissions' => [ 'update-post' => true, 'publish-post' => true, ] ]); }}
不要忘记从 DatabaseSeeder
中调用 RolesSeeder
。
$this->call(\RolesSeeder::class);
用户和角色模型
如果我们执行种子命令,它将失败,因为我们还没有设置我们的模型。让我们在 app/Role
模型中添加可填充字段,并告诉我们的模型 permissions
是一个 JSON 类型字段。我们还需要在 app/Role
和 app/User
模型之间创建关系。
class Role extends Model{ protected $fillable = [ 'name', 'slug', 'permissions', ]; protected $casts = [ 'permissions' => 'array', ]; public function users() { return $this->belongsToMany(User::class, 'role_users'); } public function hasAccess(array $permissions) : bool { foreach ($permissions as $permission) { if ($this->hasPermission($permission)) return true; } return false; } private function hasPermission(string $permission) : bool { return $this->permissions[$permission] ?? false; }}
class User extends Authenticatable{ use Notifiable; protected $fillable = [ 'name', 'email', 'password', ]; protected $hidden = [ 'password', 'remember_token', ]; public function roles() { return $this->belongsToMany(Role::class, 'role_users'); } /** * Checks if User has access to $permissions. */ public function hasAccess(array $permissions) : bool { // check if the permission is available in any role foreach ($this->roles as $role) { if($role->hasAccess($permissions)) { return true; } } return false; } /** * Checks if the user belongs to role. */ public function inRole(string $roleSlug) { return $this->roles()->where('slug', $roleSlug)->count() == 1; }}
现在我们可以安全地迁移和播种我们的数据库。
php artisan migrate --seed
身份验证
Laravel 提供了一种快速方法来使用以下命令为简单的身份验证系统创建路由和视图
php artisan make:auth
它将为我们创建控制器、视图和路由,但我们需要修改注册以添加用户角色。
注册
让我们首先使角色可用于注册视图。在 Controllers/Auth/RegisterController.php
中覆盖 showRegistrationForm
方法。
Use App/Role; ... public function showRegistrationForm(){ $roles = Role::orderBy('name')->pluck('name', 'id'); return view('auth.register', compact('roles'));}
编辑 resources/views/auth/register.blade.php
并添加一个选择输入。
<br></br>... <div class="form-group{{ $errors->has('role') ? ' has-error' : '' }}"> <label for="role" class="col-md-4 control-label">User role</label> <div class="col-md-6"> <select id="role" class="form-control" name="role" required> @foreach($roles as $id => $role) <option value="{{$id}}">{{$role}}</option> @endforeach </select> @if ($errors->has('role')) <span class="help-block"> <strong>{{ $errors->first('role') }}</strong> </span> @endif </div></div> ...
不要忘记验证我们添加的新字段。更新 RegisterController
中的 validator
方法。
<br></br>... protected function validator(array $data){ return Validator::make($data, [ 'name' => 'required|max:255', 'email' => 'required|email|max:255|unique:users', 'password' => 'required|min:6|confirmed', 'role' => 'required|exists:roles,id', // validating role ]);} ...
覆盖控制器中的 create
方法(该方法继承自 RegistersUsers
特性)并将角色附加到注册用户。
<br></br>... protected function create(array $data){ $user = User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => bcrypt($data['password']), ]); $user->roles()->attach($data['role']); return $user;} ...
更改 RegisterController
和 LoginController
中的重定向链接。
<br></br>... protected $redirectTo = '/'; ...
运行应用程序
如果您使用 php artisan serve
命令运行服务器,您将能够创建用户并将角色附加到它。访问 /register
并创建一个新用户。
定义策略
在这里,我们将定义访问策略来保护我们的操作。更新 app/Providers/AuthServiceProvider.php
以包含应用程序的策略。
use App\Post; ... public function boot(){ $this->registerPolicies(); $this->registerPostPolicies();} public function registerPostPolicies(){ Gate::define('create-post', function ($user) { return $user->hasAccess(['create-post']); }); Gate::define('update-post', function ($user, Post $post) { return $user->hasAccess(['update-post']) or $user->id == $post->user_id; }); Gate::define('publish-post', function ($user) { return $user->hasAccess(['publish-post']); }); Gate::define('see-all-drafts', function ($user) { return $user->inRole('editor'); });}
路由
现在让我们定义我们的路由;使用所有应用程序的路由更新 routes/web.php
。
Auth::routes(); Route::get('/', 'PostController@index');Route::get('/posts', 'PostController@index')->name('list_posts');Route::group(['prefix' => 'posts'], function () { Route::get('/drafts', 'PostController@drafts') ->name('list_drafts') ->middleware('auth'); Route::get('/show/{id}', 'PostController@show') ->name('show_post'); Route::get('/create', 'PostController@create') ->name('create_post') ->middleware('can:create-post'); Route::post('/create', 'PostController@store') ->name('store_post') ->middleware('can:create-post'); Route::get('/edit/{post}', 'PostController@edit') ->name('edit_post') ->middleware('can:update-post,post'); Route::post('/edit/{post}', 'PostController@update') ->name('update_post') ->middleware('can:update-post,post'); // using get to simplify Route::get('/publish/{post}', 'PostController@publish') ->name('publish_post') ->middleware('can:publish-post');});
帖子
让我们开始处理我们的帖子,好吗?
帖子模型
首先,我们定义可填充字段,然后添加 Eloquent 关系。
<br></br>... class Post extends Model{ protected $fillable = [ 'title', 'slug', 'body', 'user_id', ]; public function owner() { return $this->belongsTo(User::class); } public function scopePublished($query) { return $query->where('published', true); } public function scopeUnpublished($query) { return $query->where('published', false); }}
帖子控制器
我们已经创建了一个控制器,现在让我们让它变得有用。
列出帖子
将 index 方法添加到 PostController.php
中,以列出所有已发布的帖子。
use App\Post; ... public function index(){ $posts = Post::published()->paginate(); return view('posts.index', compact('posts'));} ...
接下来,编辑 resources/views/home.blade.php
并将其重命名为 resources/views/posts/index.blade.php
。
@extends('layouts.app') @section('content')<div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading"> Posts @can('create-post') <a class="pull-right btn btn-sm btn-primary" href="{{ route('create_post') }}">New</a> @endcan </div> <div class="panel-body"> <div class="row"> @foreach($posts as $post) <div class="col-sm-6 col-md-4"> <div class="thumbnail"> <div class="caption"> <h3><a href="{{ route('edit_post', ['id' => $post->id]) }}">{{ $post->title }}</a></h3> <p>{{ str_limit($post->body, 50) }}</p> @can('update-post', $post) <p> <a href="{{ route('edit_post', ['id' => $post->id]) }}" class="btn btn-sm btn-default" role="button">Edit</a> </p> @endcan </div> </div> </div> @endforeach </div> </div> </div> </div> </div></div>@endsection
如果我们以访客身份访问帖子页面,我们将看不到 new
按钮;只有作者才被允许看到它并访问页面。
创建帖子
让我们创建一个创建帖子页面。将以下方法添加到 PostController
中。
<br></br>... public function create(){ return view('posts.create');} ...
创建一个视图文件并将其命名为 posts\create.blade.php
。
@extends('layouts.app') @section('content')<div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">New Post</div> <div class="panel-body"> <form class="form-horizontal" role="form" method="POST" action="{{ route('store_post') }}"> {{ csrf_field() }} <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}"> <label for="title" class="col-md-4 control-label">Title</label> <div class="col-md-6"> <input id="title" type="text" class="form-control" name="title" value="{{ old('title') }}" required autofocus> @if ($errors->has('title')) <span class="help-block"> <strong>{{ $errors->first('title') }}</strong> </span> @endif </div> </div> <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}"> <label for="body" class="col-md-4 control-label">Body</label> <div class="col-md-6"> <textarea name="body" id="body" cols="30" rows="10" class="form-control" required>{{ old('body') }}</textarea> @if ($errors->has('body')) <span class="help-block"> <strong>{{ $errors->first('body') }}</strong> </span> @endif </div> </div> <div class="form-group"> <div class="col-md-6 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Create </button> <a href="{{ route('list_posts') }}" class="btn btn-primary"> Cancel </a> </div> </div> </form> </div> </div> </div> </div></div>@endsection
存储帖子
接下来,我们将创建 store
方法。
use App\Http\Requests\StorePost as StorePostRequest;use Auth; ... public function store(StorePostRequest $request){ $data = $request->only('title', 'body'); $data['slug'] = str_slug($data['title']); $data['user_id'] = Auth::user()->id; $post = Post::create($data); return redirect()->route('edit_post', ['id' => $post->id]);} ...
我们需要创建一个 StorePost
请求,以在存储帖子之前验证表单数据。很简单;执行以下 Artisan 命令。
php artisan make:request StorePost
编辑 app/Http/Requests/StorePost.php
并在 rules
方法中提供我们需要的验证。authorize
方法应该始终返回 true
,因为我们使用 Gate
中间件来进行实际的访问授权。
public function authorize(){ return true; // gate will be responsible for access} public function rules(){ return [ 'title' => 'required|unique:posts', 'body' => 'required', ];}
草稿
我们只希望作者能够创建帖子,但这些帖子在编辑发布之前不会公开访问。因此,我们将创建一个草稿或未发布帖子的页面,这些页面只能由经过身份验证的用户访问。
要显示草稿,请将 drafts
方法添加到 PostController
中。
use Gate; ... public function drafts(){ $postsQuery = Post::unpublished(); if(Gate::denies('see-all-drafts')) { $postsQuery = $postsQuery->where('user_id', Auth::user()->id); } $posts = $postsQuery->paginate(); return view('posts.drafts', compact('posts'));} ...
创建 posts/drafts.blade.php
视图。
@extends('layouts.app') @section('content')<div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading"> Drafts <a class="btn btn-sm btn-default pull-right" href="{{ route('list_posts') }}">Return</a> </div> <div class="panel-body"> <div class="row"> @foreach($posts as $post) <div class="col-sm-6 col-md-4"> <div class="thumbnail"> <div class="caption"> <h3><a href="{{ route('show_post', ['id' => $post->id]) }}">{{ $post->title }}</a></h3> <p>{{ str_limit($post->body, 50) }}</p> <p> @can('publish-post') <a href="{{ route('publish_post', ['id' => $post->id]) }}" class="btn btn-sm btn-default" role="button">Publish</a> @endcan <a href="{{ route('edit_post', ['id' => $post->id]) }}" class="btn btn-default" role="button">Edit</a> </p> </div> </div> </div> @endforeach </div> </div> </div> </div> </div></div>@endsection
我们需要创建一个链接来访问草稿页面。在 layouts/app.blade.php
中,修改下拉菜单并添加一个指向草稿页面的链接。
<br></br>... <ul class="dropdown-menu" role="menu"> <li> <a href="{{ route('list_drafts') }}">Drafts</a> ...
编辑帖子
让我们使编辑草稿和已发布的帖子成为可能。将以下方法添加到 PostController
中。
use App\Http\Requests\UpdaPost as UpdatePostRequest; ... public function edit(Post $post){ return view('posts.edit', compact('post'));} public function update(Post $post, UpdatePostRequest $request){ $data = $request->only('title', 'body'); $data['slug'] = str_slug($data['title']); $post->fill($data)->save(); return back();}
我们还需要创建一个新的 FormRequest
。我们必须使帖子标题唯一,但在更新时允许它们具有相同的标题。为此,我们使用 Laravel 的 Rule
。
请注意,由于我们将路由中的 id
绑定到 Post
模型,因此 Post
模型将可从 Request
对象访问。有关更多详细信息,请查看 官方文档。
php artisan make:request UpdatePost
use Illuminate\Validation\Rule; ... public function authorize(){ return true;} public function rules(){ $id = $this->route('post')->id; return [ 'title' => [ 'required', Rule::unique('posts')->where('id', '<>', $id), ], 'body' => 'required', ];}
创建 posts/edit.blade.php
视图
@extends('layouts.app') @section('content')<div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Update Post</div> <div class="panel-body"> <form class="form-horizontal" role="form" method="POST" action="{{ route('update_post', ['post' => $post->id]) }}"> {{ csrf_field() }} <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}"> <label for="title" class="col-md-4 control-label">Title</label> <div class="col-md-6"> <input id="title" type="text" class="form-control" name="title" value="{{ old('title', $post->title) }}" required autofocus> @if ($errors->has('title')) <span class="help-block"> <strong>{{ $errors->first('title') }}</strong> </span> @endif </div> </div> <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}"> <label for="body" class="col-md-4 control-label">Body</label> <div class="col-md-6"> <textarea name="body" id="body" cols="30" rows="10" class="form-control" required>{{ old('body', $post->body) }}</textarea> @if ($errors->has('body')) <span class="help-block"> <strong>{{ $errors->first('body') }}</strong> </span> @endif </div> </div> <div class="form-group"> <div class="col-md-6 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Update </button> @can('publish-post') <a href="{{ route('publish_post', ['post' => $post->id]) }}" class="btn btn-primary"> Publish </a> @endcan <a href="{{ route('list_posts') }}" class="btn btn-primary"> Cancel </a> </div> </div> </form> </div> </div> </div> </div></div>@endsection
发布草稿
为了简单起见,我们将通过访问一个 get
入口点来发布帖子。使用带有表单的 post
方式是最好的。将 publish
添加到 PostController
。
<br></br>... public function publish(Post $post){ $post->published = true; $post->save(); return back();} ...
显示帖子
现在,让我们使帖子可供发布。将 show
添加到 PostController
。
<br></br>... public function show($id){ $post = Post::published()->findOrFail($id); return view('posts.show', compact('post'));}
当然,视图是 posts/show.blade.php
。
@extends('layouts.app') @section('content')<div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading"> {{ $post->title }} <a class="btn btn-sm btn-default pull-right" href="{{ route('list_posts') }}">Return</a> </div> <div class="panel-body"> {{ $post->body }} </div> </div> </div> </div></div>@endsection
404
为了在用户尝试访问不存在的页面时获得干净的外观,我们需要制作一个 404 页面。我们需要创建一个新的 Blade 文件 errors/404.blade.php
,并允许用户返回到正确的页面。
<html> <body> <h1>404</h1> <a href="/">Back</a> </body></html>
结论
我们的虚拟应用程序允许每种类型的用户执行他拥有权限的精确操作。我们轻松地实现了这一点,而无需使用任何第三方包。由于 Laravel 新版本使这种方法成为可能,因此我们不必从包中继承我们不需要的功能。
我们的应用程序中的业务逻辑与访问逻辑分离;这使得维护更加容易。如果我们的 ACL 规范发生变化,我们可能根本不需要接触控制器。
下次我将发布一篇关于使用 Gate 与第三方 ACL 包的文章。