Laravel 授权门概述

发布于 作者

An Introduction to Laravel Authorization Gates image

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=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dbname
DB_USERNAME=dbuser
DB_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');
});

我们需要添加几个其他表:rolesuser_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/Roleapp/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;
}
 
...

更改 RegisterControllerLoginController 中的重定向链接。

<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 包的文章。

Yazid Hanifi photo

开发者、机器学习学生、定期踢足球。

Cube

Laravel 新闻

加入 40,000 多名其他开发者,绝不错过任何新的技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发者的必备代码运行器。使用 AI、自动完成和对本地和生产环境的即时反馈进行调试。

Tinkerwell
No Compromises logo

无妥协

Joel 和 Aaron,来自 No Compromises 播客的两位经验丰富的开发人员,现在可以为您的 Laravel 项目聘用。 ⬧ 固定费率 $7500/月。 ⬧ 无漫长的销售流程。 ⬧ 无合同。 ⬧ 100% 退款保证。

无妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序成功。

Kirschbaum
Shift logo

Shift

运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,以保持您的应用程序新鲜。

Shift
Bacancy logo

Bacancy

只需每月 $2500,即可为您的项目配备一位经验丰富的 Laravel 开发人员,拥有 4-6 年的经验。获得 160 小时的专业知识和 15 天的无风险试用。立即安排电话!

Bacancy
Lucky Media logo

Lucky Media

立即获得幸运 - Laravel 开发的理想选择,拥有超过十年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源包,将现代无头电子商务功能的强大功能带入 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 工作板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit:Laravel SaaS 启动套件

SaaSykit 是一个 Laravel SaaS 启动套件,包含运行现代 SaaS 所需的所有功能。付款、漂亮的结账、管理面板、用户仪表板、身份验证、就绪组件、统计信息、博客、文档等等。

SaaSykit:Laravel SaaS 启动套件
Rector logo

Rector

您的无缝 Laravel 升级合作伙伴,降低成本,加速创新,助您公司取得成功

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 如何灵活、现代的数据库可以改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的无依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包为您的 Laravel 应用程序添加评论

阅读文章