文章详情

短信预约信息系统项目管理师 报名、考试、查分时间动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Laravel 角色和权限:拦截器(Gates)和策略(Policies)解释

admin

admin

2022-05-31 11:16

关注

在 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.添加从用户到角色的关系:一对多或多对多

  1. 播种默认角色并将其分配给现有用户

  2. 在注册时分配一个默认角色

  3. 更改 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 查询问题


使其保持灵活:保存在数据库中的权限

以我个人的经验,将它们一起构建的通常模型是这样的:

$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 PermissionBouncer,我有一个 单独关于它们的长篇文章。虽然文章很老了(发布挺久了),但在市场上,他们依然是这个话题领域的领导者。没错,这主要还是归结于它们非常稳定。

这些包的作用是帮助你将权限管理抽象为一种对人类友好的语言,使用你可以轻松记住和使用的方法。

从 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 项目,你永远不需要更改守卫,甚至不需要知道它们是如何工作的。守卫不在此角色/权限主题讨论的范围内。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯