在 Laravel 中,角色和权限多年来一直是最令人困惑的话题之一。大多数情况下,因为没有关于这些内容的文档:在框架中这类概念被用其他术语表述取代了,以至于我们没法简单的去理解这些内容。像这种概念术语有:「Gates(门面 / 拦截器)」、「Policies(策略)」、「Guards(守卫)」等。在本文中,我将尝试用「人话」将它们全部解释清楚。

Gate(门面 / 拦截器)与 Permission(许可)概念相同

在我看来,最大的困惑之一是「Gate」这个词。我认为如果他们能被清楚的解释他们是什么,开发人员会避免很多混乱。

Gates(门面 / 拦截器) 是 Permissions(许可),只是用另一个词来称呼。

我们需要使用权限执行哪些典型操作?

  • 定义权限,例如:管理用户
  • 检查前端的权限,例如:显示 / 隐藏按钮
  • 检查后端的权限,例如:可以 / 不能更新数据

所以,把「Permission(许可)」跟「Gate(门面 / 拦截器)」两个概念做一个替换,你就明白了。

一个简单的 Laravel 示例如下:

app/Providers/AppServiceProvider.php:

use AppModelsUser;
use IlluminateSupportFacadesGate;

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 查询问题


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

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

  • 所有权限和角色都保存在数据库中,通过一些管理面板进行管理;
  • 关系:角色多对多权限,用户属于角色(或多对多角色);
  • 然后,在 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 项目,你永远不需要更改守卫,甚至不需要知道它们是如何工作的。守卫不在此角色 / 权限主题讨论的范围内。