Laravel角色和权限:拦截器Gates和策略Policies的解释
在 Laravel 中,角色和权限多年来一直是最令人困惑的话题之一。
大多数情况下,因为没有关于它的文档:相同的东西“隐藏”在框架中的其他术语下,如“gates”、“policies”、“guards”等。
在本文中,我将尝试将它们全部解释 “人类语言”。
门(gate)与权限(Permission)相同
在我看来,最大的困惑之一是“门”这个词。
我认为如果他们被称为他们是什么,开发人员会避免很多混乱。
gate是权限,只是用另一个词来称呼。
我们需要使用权限执行哪些典型操作?
定义权限,例如。 “管理用户”
检查前端的权限,例如。 显示/隐藏按钮
检查后端的权限,例如。 可以/不能更新数据
所以,是的,把“许可”这个词换成“门”,你就明白了。
一个简单的 Laravel 示例如下:
app/Providers/AppServiceProvider.php:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
// Should return TRUE or 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 包中一样。
这也意味着同样的事情——某些行动的能力/许可。 我们将在本文后面介绍这些包。
https://laravel-news.com/bouncer-authorization-package
检查门权限的各种方法
另一个混乱的来源是如何/在哪里检查门。 它非常灵活,您可能会发现非常不同的示例。
让我们来看看它们:
Option 1. Routes: middleware('can:xxxxxx')
这是上面的例子。 直接在路由/组上,您可以分配中间件:
Route::post('users', [UserController::class, 'store'])
->middleware('can:create_users');
Option 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);
}
}
Option 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);
}
Option 4. Controller: authorize()
更短的选择,也是我最喜欢的选择,是在控制器中使用 authorize()。 如果失败,它会自动返回一个 403 页面。
public function store(Request $request)
{
$this->authorize('create_users');
}
Option 5. Form Request class:
我注意到许多开发人员生成表单请求类只是为了定义验证规则,完全忽略了该类的第一个方法,即 authorize()。
您也可以使用它来检查大门。 这样,您就实现了关注点分离,这对于可靠的代码来说是一个很好的做法,因此控制器不负责验证,因为它是在其专用的表单请求类中完成的。
public function store(StoreUserRequest $request)
{
// No check is needed in the Controller method
}
然后,在表单请求中:
class StoreProjectRequest extends FormRequest
{
public function authorize()
{
return Gate::allows('create_users');
}
public function rules()
{
return [
// ...
];
}
}
策略:基于模型的权限集
如果您的权限可以分配给 Eloquent 模型,那么在典型的 CRUD 控制器中,您可以围绕它们构建一个 Policy 类。
如果我们运行这个命令:
php artisan make:policy ProductPolicy --model=Product
它将生成文件 app/Policies/UserPolicy.php,默认方法有注释来解释其用途:
use App\Models\Product;
use App\Models\User;
class ProductPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user)
{
//
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Product $product)
{
//
}
/**
* Determine whether the user can create models.
*/
public function create(User $user)
{
//
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Product $product)
{
//
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Product $product)
{
//
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Product $product)
{
//
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Product $product)
{
//
}
}
在这些方法中的每一个中,您都定义了真/假返回的条件。 所以,如果我们按照之前盖茨的例子,我们可以这样做:
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 文档中,您不会找到任何有关用户角色的部分。
原因很简单:
术语“角色”是人为组成的,将权限分组到某种名称下,例如“管理员”或“编辑”。
从框架的角度来看,没有“角色”,只有您可以以任何您想要的方式分组的门/策略。
换句话说,角色是 Laravel 框架之外的一个实体,所以我们需要自己构建角色结构。
它可能是整个身份验证混乱的一部分,但它非常有意义,因为我们应该控制角色的定义方式:
是一个角色还是多个角色?
一个用户可以有一个角色还是多个角色?
谁可以管理系统中的角色?
等等
因此,角色功能是 Laravel 应用程序的另一层。
这是我们获得可能有帮助的 Laravel 软件包的地方。
但是我们也可以创建没有任何包的角色:
创建“角色”数据库表和角色雄辩模型
添加从用户到角色的关系:一对多或多对多
播种默认角色并将其分配给现有用户
在注册时分配默认角色
更改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;
}
}
// Every permission may have multiple roles assigned
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,我有一篇单独的长篇文章。 文章很老了,但市场领导者还是一样的,因为他们很稳定。
https://github.com/spatie/laravel-permission
https://laravel-news.com/two-best-roles-permissions-packages
这些包的作用是帮助您将权限管理抽象为一种对人类友好的语言,使用您可以轻松记住和使用的方法。
从 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 链接或我上面的文章中阅读有关如何使用这些软件包的更多信息。
因此,这些包是我们在本文中介绍的身份验证/授权的最后“层”,我希望您现在能够全面了解并能够选择使用什么策略。
附言等等,守卫呢?
哦,那些。
多年来,它们引起了很多混乱。
许多开发人员认为 Guards 是角色,并开始创建单独的数据库表,如“管理员”,然后将它们分配为 Guards。
部分是因为在文档中你可能会发现像
Auth::guard('admin')->attempt($credentials)) 这样的代码片段
我什至向文档提交了一个 Pull Request 并发出警告以避免这种误解。
https://github.com/laravel/docs/pull/6750
在官方文档中,你可能会发现这一段:
Laravel 的认证设施的核心是由“守卫”和“提供者”组成。
Guards 定义了如何为每个请求对用户进行身份验证。
例如,Laravel 附带了一个会话守卫,它使用会话存储和 cookie 来维护状态。
所以,守卫是一个比角色更全球化的概念。
守卫的一个示例是“会话”,在文档的后面,您可能会看到 JWT 守卫示例。
换句话说,守卫是一种完整的身份验证机制,对于大多数 Laravel 项目,您永远不需要更改守卫,甚至不需要知道它们是如何工作的。
警卫不在此角色/权限主题范围内.
相关文章