在 Laravel 中,角色和权限多年来一直是最令人困惑的话题之一。大多数情况下,因为没有关于这些内容的文档:在框架中这类概念被用其他术语表述取代了,以至于我们没法简单的去理解这些内容。像这种概念术语有:「Gates(门面/拦截器)」、「Policies(策略)」、「Guards(守卫)」等。在本文中,我将尝试用「人话」将它们全部解释清楚。
Gate(门面/拦截器)与 Permission(许可)概念相同
在我看来,最大的困惑之一是「Gate」这个词。我认为如果他们能被清楚的解释他们是什么,开发人员会避免很多混乱。
Gates(门面/拦截器) 是 Permissions(许可),只是用另一个词来称呼。
我们需要使用权限执行哪些典型操作?
定义权限,例如:管理用户
检查前端的权限,例如:显示/隐藏按钮
检查后端的权限,例如:可以/不能更新数据
所以,把「Permission(许可)」跟「Gate(门面/拦截器)」两个概念做一个替换,你就明白了。
一个简单的 Laravel 示例如下:
app/Providers/AppServiceProvider.php:
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
// 应该返回 TRUE 或 FALSE
Gate::define('manage_users', function(User $user) {
return $user->is_admin == 1;
});
}
}
resources/views/navigation.blade.php:
<ul>
<li>
<a href="{{ route('projects.index') }}">Projects</a>
</li>
@can('manage_users')
<li>
<a href="{{ route('users.index') }}">Users</a>
</li>
@endcan
</ul>
routes/web.php:
Route::resource('users', UserController::class)->middleware('can:manage_users');
当然,我也知道,从技术上讲,Gate(门面)可能意味着不止一项权限。因此你可以定义类似「admin_area」的内容,而不是「manage_users」。但在我见过的大多数例子中,Gate(门面)是 Permission(许可)的同义词。
此外,在某些情况下,这些权限称为「能力」,例如在 Bouncer 包 中。这也意味着同样的事情——某些行动的能力/许可。我们将在本文后面介绍这些包。
检查 Gate 权限的各种方法
另一个混乱问题的来源是如何/在哪里检查 Gate。它非常灵活,你可能会发现非常不同的示例。让我们来看看它们:
选项 1. 路由 Route: middleware(‘can:xxxxxx’)
这是上面的例子。直接在路由/组上,你可以分配中间件:
Route::post('users', [UserController::class, 'store'])
->middleware('can:create_users');
选项 2. 控制器 Controller: can() / cannot()
在 Controller 方法的第一行,我们可以看到类似这样的内容,使用方法 can()
或 cannot()
,与 Blade 指令相同:
public function store(Request $request){ if (!$request->user()->can('create_users')) abort(403); }}
与之相反的是 cannot()
:
public function store(Request $request){ if ($request->user()->cannot('create_users')) abort(403); }}
或者,如果你没有 $request
变量,您可以使用 auth()
助手:
public function create(){ if (!auth()->user()->can('create_users')) abort(403); }}
选项 3. Gate::allows() or Gate::denies()
另一种方法是使用 Gate 门面:
public function store(Request $request){ if (!Gate::allows('create_users')) { abort(403); }}
或者,相反的方式:
public function store(Request $request){ if (Gate::denies('create_users')) { abort(403); }}
或者,使用更短的助手函数方法:
public function store(Request $request){ abort_if(Gate::denies('create_users'), 403);}
选项 4. 控制器 Controller: authorize()
使用更简单的写法,也是我最喜欢的选项,是在控制器中使用 authorize()
。如果失败,它会自动返回一个 403 页面。
public function store(Request $request){
$this->authorize('create_users');}
选项 5. 表单请求类:
我注意到许多开发人员生成 表单请求类 只是为了定义验证规则,完全忽略了第一种方法类,即 authorize()
。
你也可以使用它来检查门面。这样,你就实现了关注点分离,这对于可靠的代码来说是一个很好的做法,因此控制器不负责验证,因为它是在其专用的表单请求类中完成的。
public function store(StoreUserRequest $request){ // Controller 方法中不需要检查}
然后,在表单请求中:
class StoreProjectRequest extends FormRequest{ public function authorize() { return Gate::allows('create_users'); } public function rules() { return [ // ... ]; }}
Policy(策略):基于模型的权限集
如果你的权限可以分配给 Eloquent 模型,那么在典型的 CRUD 控制器中,你可以围绕它们构建一个 Policy 类。
如果我们运行这个命令:
php artisan make:policy ProductPolicy --model=Product
它将生成文件 app/Policies/UserPolicy.php,其中默认方法有注释来解释其用途:
use AppModelsProduct;use AppModelsUser;class ProductPolicy{
use HandlesAuthorization; public function viewAny(User $user) { // } public function view(User $user, Product $product) { // } public function create(User $user) { // } public function update(User $user, Product $product) { // } public function delete(User $user, Product $product) { // } public function restore(User $user, Product $product) { // } public function forceDelete(User $user, Product $product) { // }}
在这些方法中的每一个中,你都定义了 true/false 返回的条件。所以,如果我们按照之前 Gates(门面)的例子,我们可以这样做:
class ProductPolicy{ public function create(User $user) { return $user->is_admin == 1; }
然后,你可以使用与 Gates 非常相似的方式检查 Policy:
public function store(Request $request){
$this->authorize('create', Product::class);}
因此,你指定策略的方法名称和类名称。
换句话说,Policies 只是对权限进行分组的另一种方式,而不是 Gates。如果你的操作主要围绕模型的 CRUD,那么策略可能是比 Gates 更方便且结构更好的选择。
角色:通用权限集
让我们讨论另一个困惑:在 Laravel 文档中,你不会找到任何有关用户角色的部分。原因很简单:术语「Role(角色)」是人为组成的,将权限分组到某种名称下,例如「管理员」或「编辑」。
从框架的角度来看,没有「角色」,你可以以任何你想要的方式分组的门面/策略。
换句话说,角色是 Laravel 框架之外的一个实体,所以我们需要自己构建角色结构。它可能是让整个身份验证混乱的一部分,但它非常有意义,因为我们应该控制角色的定义方式:
是一个角色还是多个角色?
一个用户可以有一个角色还是多个角色?
谁可以管理系统中的角色?
其他
因此,角色功能是 Laravel 应用程序的另一层。这是我们获得可能有帮助的 Laravel 软件包的地方。但是我们也可以创建没有任何包的角色:
1.创建「角色」数据库表和角色 Eloquent 模型
2.添加从用户到角色的关系:一对多或多对多
播种默认角色并将其分配给现有用户
在注册时分配一个默认角色
更改 Gates(门面)/ Policies(策略) 以检查角色
最后一点是最关键的。
因此,你的代码不应该像下面这样:
class ProductPolicy{ public function create(User $user) { return $user->is_admin == 1; }
你需要这样处理:
class ProductPolicy{ public function create(User $user) { return $user->role_id == Role::ADMIN; }
同样的道理,在这里你有几个方式选择来检查角色。在上面的例子中,我们假设 User 和 Role 之间存在一个 belongsTo
的关系,并且 Role 模型中还有一些常量,比如 ADMIN = 1
,比如 EDITOR = 2
,只是为了避免过多地查询数据库。
但是如果你喜欢灵活一点,你可以每次都查询数据库:
class ProductPolicy{ public function create(User $user) { return $user->role->name == 'Administrator'; }
但请记住要预先加载「角色」关系,否则,你很容易在此处遇到 N+1 查询问题。
使其保持灵活:保存在数据库中的权限
以我个人的经验,将它们一起构建的通常模型是这样的:
所有权限和角色都保存在数据库中,通过一些管理面板进行管理;
关系:角色多对多权限,用户属于角色(或多对多角色);
然后,在 AppServiceProvider 中,你从 DB 的所有权限中创建一个
foreach
循环,并为每个权限运行Gate::define()
语句,根据角色返回 true/false;最后,你使用
@can('permission_name')
和$this->authorize('permission_name')
检查权限,就像上面的示例一样。
$roles = Role::with('permissions')->get();$permissionsArray = [];foreach ($roles as $role) { foreach ($role->permissions as $permissions) {
$permissionsArray[$permissions->title][] = $role->id; }}// 每个权限都可能分配有多个角色foreach ($permissionsArray as $title => $roles) {
Gate::define($title, function ($user) use ($roles) { // We check if we have the needed roles among current user's roles return count(array_intersect($user->roles->pluck('id')->toArray(), $roles)) > 0; });}
换句话说,我们不检查角色的任何访问。角色只是「人为设计出来的一层逻辑,或者理解为一组权限,在应用程序生命周期中转化为 Gates。
看起来很复杂?不用担心,我们可以通过第三方扩展的包解决这个问题。
管理角色/权限的第三方扩展包
最受欢迎的软件包是 Spatie Laravel Permission 和 Bouncer,我有一个 单独关于它们的长篇文章。虽然文章很老了(发布挺久了),但在市场上,他们依然是这个话题领域的领导者。没错,这主要还是归结于它们非常稳定。
这些包的作用是帮助你将权限管理抽象为一种对人类友好的语言,使用你可以轻松记住和使用的方法。
从 Spatie 许可的操作看这个漂亮的语法:
$user->givePermissionTo('edit articles');$user->assignRole('writer');$role->givePermissionTo('edit articles');$user->can('edit articles');
Bouncer 可能不太直观,但仍然非常好:
Bouncer::allow($user)->to('create', Post::class);Bouncer::allow('admin')->to('ban-users');Bouncer::assign('admin')->to($user);
你可以在他们的 Github 链接或我上面的文章中阅读有关如何使用这些软件包的更多信息。
因此,这些包是我们在本文中介绍的身份验证/授权的最后 Layer(层),我希望你现在能够全面了解并能够选择使用什么策略。
P.S. 等等,Guards(守卫)呢?
哦,那么多年来,这么多概念引起了很多混乱。许多开发人员认为 Guards 是角色,并开始创建单独的数据库表,如「管理员」,然后将它们分配为 Guards。部分原因是因为在文档中你可能会找到类似 Auth::guard('admin')->attempt($credentials))
的代码片段。
我甚至 向 Laravel 文档提交了 Pull Request 并发出警告以避免这种误解。
在官方文档中,你可能会发现这一段:
在其核心,Laravel 的认证设施由「Guards(守卫)」和「providers(提供者)」组成。 Guards 定义了如何为每个请求对用户进行身份验证。例如,Laravel 附带了一个会话守卫,它使用会话存储和 cookie 来维护状态。
所以,守卫是一个比角色更全球化的概念。守卫的一个示例是「会话」,在文档的后面,你可能会看到 JWT 守卫示例。换句话说,守卫是一种完整的身份验证机制,对于大多数 Laravel 项目,你永远不需要更改守卫,甚至不需要知道它们是如何工作的。守卫不在此角色/权限主题讨论的范围内。