Laravel 角色和权限:详解 Gates 和 Policies
发布日期:作者: PovilasKorop
在 Laravel 中,角色和权限多年来一直是最令人困惑的话题之一。主要是因为没有关于它的文档:相同的内容在框架中以其他术语“隐藏”,比如“gates”、“policies”、“guards”等等。在这篇文章中,我将尝试用“人类语言”解释所有这些。
Gate 等同于 Permission
我认为,“gate”这个术语是最大的困惑之一。我认为如果它们被称为它们是什么,开发者就会避免很多困惑。
Gates 就是 **Permissions**,只是叫法不同。
我们使用权限需要执行哪些典型操作?
- 定义权限,例如“manage_users”。
- 在前端检查权限,例如显示/隐藏按钮。
- 在后端检查权限,例如是否可以更新数据。
所以是的,将“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 的同义词。
此外,在某些情况下,权限称为“abilities”,例如在 Bouncer 包 中。它也表示相同的意思 - 某些操作的能力/权限。我们将在本文的后面部分谈到这些包。
检查 Gate 权限的各种方法
另一个困惑的来源是检查 Gate 的方式/位置。它非常灵活,因此你可能会找到非常不同的示例。让我们逐一浏览它们。
选项 1. 路由:middleware('can:xxxxxx')
这是上面的示例。你可以在路由/组上直接分配中间件。
Route::post('users', [UserController::class, 'store']) ->middleware('can:create_users');
选项 2. 控制器:can() / cannot()
在控制器方法的第一行,我们可以看到类似这样的内容,使用 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() 或 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. 控制器:authorize()
更简短的选项,也是我个人最喜欢的选项,是在控制器中使用 authorize()
。如果失败,它会自动返回 403 页面。
public function store(Request $request){ $this->authorize('create_users');}
选项 5. 表单请求类:
我注意到,许多开发人员生成 表单请求类 只是为了定义验证规则,完全忽略了该类的第一个方法 authorize()
。
你也可以使用它来检查 gates。这样,你就可以实现关注点分离,这对稳固的代码来说是一种良好的实践,因此控制器不会负责验证,因为它在专门的表单请求类中完成。
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 [ // ... ]; }}
Policy:基于模型的权限集
如果你的权限可以分配给 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) { // }}
在每个方法中,你定义 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);}
因此,你指定方法名称和 Policy 的类名称。
换句话说,Policies 只是另一种分组权限的方式,而不是 Gates。如果你的操作主要围绕模型的 CRUD,那么 Policies 可能比 Gates 更方便、结构更合理。
Role:通用权限集
让我们讨论另一个令人困惑的地方:在 Laravel 文档中,你找不到任何关于用户角色的部分。原因很简单:“roles”这个词是人为造出来的,用来将权限分组到某种名称下,比如“administrator”或“editor”。
从框架的角度来看,不存在“roles”,只有 gates/policies,你可以按任何你想要的方式进行分组。
换句话说,角色是 Laravel 框架**外部**的实体,因此我们需要自己构建角色结构。这可能是整体身份验证困惑的一部分,但这是有道理的,因为我们应该控制角色的定义方式
- 是单个角色还是多个角色?
- 用户可以拥有一个角色还是多个角色?
- 谁可以管理系统中的角色?
- 等等。
因此,Role 功能是 Laravel 应用程序的另一层。这就是我们可以使用 Laravel 包来提供帮助的地方。但是我们也可以在没有任何包的情况下创建角色
- 创建“roles”数据库表和 Role Eloquent 模型
- 从 User 到 Role 添加关系:一对多或多对多
- 播种默认角色并将它们分配给现有用户
- 在注册时分配默认角色
- 更改 Gates/Policies 以检查 Role 而不是其他内容
最后一点是最重要的。
因此,而不是
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'; }
但请记住要急切加载“role”关系,否则你很容易遇到 N+1 查询问题。
使其灵活:将权限保存在数据库中
根据我的个人经验,将所有内容构建在一起的常用模型是:
- 所有权限和角色都保存在数据库中,由某个管理面板进行管理;
- 关系:角色多对多权限,User 属于 Role(或多对多角色);
- 然后,在 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 assignedforeach ($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 或我上面的文章来了解更多关于如何使用这些包的信息。
因此,这些包是我们在这篇文章中介绍的身份验证/授权的最终“层”,我希望您现在能够全面了解并选择要使用的策略。
附注:等等,守卫怎么办?
哦,那些。多年来,它们造成了很多混乱。许多开发人员认为守卫就是角色,并开始创建诸如“管理员”之类的独立数据库表,然后将这些表分配为守卫。部分原因是,您可能会在文档中找到类似于 Auth::guard('admin')->attempt($credentials))
的代码片段。
我甚至向文档提交了一个 Pull Request,以警告避免这种误解。
在官方文档中,您可能会找到以下段落
从本质上讲,Laravel 的身份验证功能由“守卫”和“提供程序”组成。守卫定义了每个请求如何对用户进行身份验证。例如,Laravel 附带一个会话守卫,它使用会话存储和 cookie 来维护状态。
因此,守卫比角色更全局的概念。守卫的一个示例是“会话”,稍后在文档中,您可能会看到一个 JWT 守卫示例。换句话说,守卫是一种完整的身份验证机制,对于大多数 Laravel 项目来说,您永远不需要更改守卫,甚至不需要了解它们的工作原理。守卫位于角色/权限主题之外。