在Laravel应用程序中管理用户权限的完整指南

Laravel框架
452
1
1
2023-07-29

在网络开发领域,你会经常遇到 "角色 "和 "权限 "这两个术语,但它们是什么意思?权限是对某一事物的访问权,比如说网络应用中的一个页面。一个角色只是一个权限的集合。

为了说明这个问题,让我们举一个内容管理系统(CMS)的简单例子。该系统可以有多种基本权限,包括以下内容。

  • 可以创建博客文章
  • 可以更新博客文章
  • 可以删除博客文章
  • 可以创建用户
  • 可以更新用户
  • 可以删除用户

该系统还可以有一些角色,比如说以下。

  • 编辑
  • 管理员

因此,我们可以假设 "编辑 "角色会有 "可以创建博文"、"可以更新博文"、"可以删除博文 "的权限。但是,他们不会有创建、更新或删除用户的权限,而管理员会有所有这些权限。

使用像上面列出的角色和权限是建立一个系统的好方法,能够限制用户能看到和做什么。

如何使用Spatie Laravel 权限包

在你的Laravel应用中,有不同的方式来实现角色和权限。你可以自己写代码来处理整个概念。然而, 这有时是非常耗时的, 在大多数情况下, 使用一个包是绰绰有余的.

在这篇文章中, 我们将使用SpatieLaravel Permission包.

安装和配置

为了开始使用这个包, 我们将使用以下命令来安装它:

composer require spatie/laravel-permission

现在我们已经安装了这个包, 我们需要发布数据库迁移和配置文件:

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

现在我们可以运行迁移,在我们的数据库中创建新的表。

php artisan migrate

假设我们使用的是默认的配置值,并且没有在软件包的config/permission.php ,那么现在我们的数据库中应该有五个新表。

  1. roles - 这个表将保存你的应用程序中的角色名称。
  2. permissions - 这张表将保存你的应用程序中的权限名称。
  3. model_has_permissions - 这个表将保存显示你的模型(例如, )拥有哪些权限的数据。User
  4. model_has_roles - 这张表将持有显示你的模型(例如, )拥有哪些角色的数据。User
  5. role_has_permissions - 这张表将持有显示每个角色所拥有的权限的数据。

为了完成基本的安装,我们现在可以将HasRoles 特质添加到我们的模型。

use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;

    // ...
}

创建角色和权限

要开始添加我们的角色和权限到我们的Laravel应用程序,我们需要首先将它们存储在数据库中。创建一个新的角色或权限是很简单的, 因为, 在Spatie的包里, 他们只是模型:Spatie\Permission\Models\RoleSpatie\Permission\Models\Permission.

所以,这意味着,如果我们想在我们的系统中创建一个新的角色,我们可以做如下的事情。

$role = Role::create(['name' => 'editor']);

我们可以用类似的方式创建权限。

$permission = Permission::create(['name' => 'create-blog-posts']);

在大多数情况下,你会在你的代码中定义权限,而不是让你的应用程序的用户创建它们。然而,你可能会对角色采取稍微不同的方法。你可能想在你的代码库中自己定义所有的角色,而不给你的用户任何创建新角色的能力。另一方面,你可以自己创建一些 "播种者 "角色(例如,管理员),然后为你的用户提供添加新角色的功能。这个决定主要归结于你想用你的系统实现什么,以及谁是最终用户。

如果你想在你的应用程序中添加任何默认的角色和权限,你可以使用数据库种子程序来添加它们。你可能想专门为这个任务创建一个播种机(也许叫RoleAndPermissionSeeder )。所以,让我们先用下面的命令来制作新的播种机。

php artisan make:seeder RoleAndPermissionSeeder

这应该已经创建了一个新的/database/seeders/RoleAndPermissionSeeder.php 文件。在我们对这个文件做任何修改之前,我们需要记住更新我们的database/seeders/DatabaseSeeder.php ,以便在我们使用php artisan db:seed 命令时,它能自动调用我们的新种子文件。

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // ...

        $this->call([
            RoleAndPermissionSeeder::class,
        ]);

        // ...
    }
}

现在,我们可以更新我们的新种子文件,向我们的系统添加一些默认的角色和权限。

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class RoleAndPermissionSeeder extends Seeder
{
    public function run()
    {
        Permission::create(['name' => 'create-users']);
        Permission::create(['name' => 'edit-users']);
        Permission::create(['name' => 'delete-users']);

        Permission::create(['name' => 'create-blog-posts']);
        Permission::create(['name' => 'edit-blog-posts']);
        Permission::create(['name' => 'delete-blog-posts']);

        $adminRole = Role::create(['name' => 'Admin']);
        $editorRole = Role::create(['name' => 'Editor']);

        $adminRole->givePermissionTo([
            'create-users',
            'edit-users',
            'delete-users',
            'create-blog-posts',
            'edit-blog-posts',
            'delete-blog-posts',
        ]);

        $editorRole->givePermissionTo([
            'create-blog-posts',
            'edit-blog-posts',
            'delete-blog-posts',
        ]);
    }
}

给用户分配角色和权限

现在我们的数据库中已经有了我们的角色和权限,并准备好分配,我们可以看看如何把它们分配给我们的用户。

首先,让我们看看给一个用户分配一个新的角色是多么简单。

$user = User::first();

$user->assignRole('Admin');

我们还可以给该角色赋予权限,这样用户也将拥有该权限。

$role = Role::findByName('Admin');

$role->givePermissionTo('edit-users');

你有可能在你的应用程序中提供权限直接分配给用户的功能,以及(或代替)将其分配给角色。下面的代码片段显示了我们如何做到这一点。

$user = User::first();

$user->givePermissionTo('edit-users');

除了能够分配角色和权限之外,你还需要提供删除角色和撤销用户权限的功能。下面是一个快速查看从一个用户身上删除角色的方法。

$user = User::first();

$user->removeRole('Admin');

我们也可以用类似的方式从用户和角色中删除权限。

$role = Role::findByName('Admin');

$role->revokePermissionTo('edit-users');

$user = User::first();

$user->revokePermissionTo('edit-users');

基于权限的访问限制

现在我们已经在数据库中存储了我们的角色和权限,并且知道如何将它们分配给我们的用户,我们可以看一下如何添加授权检查。

你可能想添加授权的第一个方法是通过使用\Illuminate\Auth\Middleware\Authorize 中间件.这在新的Laravel安装中是默认的, 所以只要你没有把它从你的app/Http/Kernel.php ,它应该被别名为can 。所以, 让我们想象一下,我们有一个路由,我们想限制访问,除非认证的用户有create-users 的中间件。我们可以将中间件添加到单个路由中。

Route::get(
    '/users/create',
    [\App\Http\Controllers\UserController::class, 'create']
)->middleware('can:create-users');

你可能会发现,你有多个路由,这些路由相互关联,并依赖相同的权限。在这种情况下,你的路由文件可能会因为逐个路由分配中间件而变得有点混乱。所以,你可以通过将中间件添加到路由组来代替授权。

Route::middleware('create-users')->group(function () {
    Route::get(
        '/users/create',
        [\App\Http\Controllers\UserController::class, 'create']
    );

    Route::post(
        '/users',
        [\App\Http\Controllers\UserController::class, 'store']
    );
});

值得注意的是,如果你喜欢在控制器构造函数中定义你的中间件,你也可以在那里使用can 中间件。你可能还想在你的控制器使用中间件的方法中利用->authorize() 。使用这种方法将需要你为你的模型创建策略,但如果使用得当,这种技术对于保持你的授权的简洁性和可理解性真的很有用。

你可能会发现在你的应用程序中,你有时需要手动检查一个用户是否有一个特定的权限,但又不完全拒绝访问。我们可以使用User 模型上的->can() 方法来做到这一点。

例如,让我们想象一下,我们的应用程序中有一个表单,允许用户更新他们的姓名、电子邮件地址和密码。现在,假设我们想给拥有 "编辑 "角色的用户以编辑用户的权限,但不能改变另一个用户的密码。我们只允许用户更新另一个用户的密码,如果他们也有edit-passwords 的权限。

在我们下面的例子中,我们将假设我们使用中间件,只允许具有edit-users 权限的用户访问这个方法。让我们来看看我们如何在控制器中实现这一点。

namespace App\Http\Controllers;

use App\Http\Requests\UpdateUserRequest;
use App\Models\User;
use Illuminate\View\View;

class UserController extends Controller
{
    // ...

    public function update(UpdateUserRequest $request, User $user): View{
        $user->name = $request->name;
        $user->email = $request->email;

        if (auth()->user()->can('edit-passwords')) {
            $user->password = $request->password;
        }

        $user->save();

        return view('users.show')->with([
            'user' => $user,
        ]);
    }

    // ...
}

基于权限在视图中显示和隐藏内容

你很可能希望能够根据用户的权限来显示和隐藏视图的一部分。例如,让我们想象一下,在我们的Blade视图中有一个基本的按钮,我们可以按下它来删除一个用户。我们还可以说,只有当用户有delete-users 的权限时,该按钮才会显示。

要显示和隐藏这个按钮,是非常简单的!我们可以使用 Blade的直接命令。我们可以使用@can() Blade指令。

@can('delete-users')
    <a href="/users/1/destroy">Delete</a>
@endcan

如果用户有delete-users 的权限,@can()@endcan 内的任何内容都会被显示。否则,它不会在Blade视图中被呈现为HTML。

重要的是要记住,在你的视图中隐藏按钮、表单和链接并不提供任何服务器端的授权。你仍然需要在你的后端代码中添加授权(例如,在你的控制器中或使用中间件,如上所述),以防止恶意用户对只有具有特定权限的用户才能使用的路由提出任何请求。

如何添加一个 "超级管理员 "的权限

当你创建一个应用程序时,你可能想添加一个 "超级管理员 "角色。一个完美的例子是,你提供一个多租户的软件即服务(SaaS)平台。你可能希望你公司的员工能够在整个应用中移动,并查看不同租户的系统(也许是为了调试和回答支持票)。

在我们添加超级管理员检查之前, 也许值得快速看一下Spatie的包是如何在Laravel中使用的.如果你还没有接触过它们, 门是非常简单的; 它们只是 "确定一个用户是否被授权执行一个给定的动作的闭包".

当你使用一段代码如$user->can('delete-users') ,你就在使用Laravel的门。

在任何闸门被运行以检查权限之前, 我们可以运行我们在before() 方法中定义的代码.如果任何一个before() 闭包运行后返回true ,用户就被允许访问。如果一个before() 闭包返回false, 它就拒绝了访问。如果它返回null, Laravel将继续运行任何未完成的before() 闭包,然后检查门本身。

在包中的\Spatie\Permission\PermissionRegistrar 类中, 我们可以看到我们的权限检查被添加为before() ,在门前运行。如果包确定用户有权限(直接分配或通过角色),它将返回true 。否则,它将返回null ,这样就可以运行任何其他的before() 关闭。

因此,我们可以用同样的方法在我们的代码中添加超级管理员角色检查。我们可以将代码添加到我们的AuthServiceProvider

use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // ...

        Gate::before(function ($user, $ability) {
            return $user->hasRole('super-admin') ? true : null;
        });

        /// ...
    }
}

现在,每当我们运行像$user->can('delete-users') 这样的一行代码时,我们将检查用户是否有delete-users 的权限或super-admin 的角色。如果这两个条件中至少有一个得到满足,用户将被允许访问。否则,该用户将被拒绝访问。

如何测试权限和访问

拥有一个涵盖你的授权的自动测试套件是非常方便的!它可以帮助你相信,你的授权是正确的。它有助于给你信心,你正在正确地保护你的路线,只有拥有正确权限的用户才能访问某些功能。

为了看看我们如何写一个测试,我们先想象一个简单的系统,我们可以为其写测试。这些测试将只是超级基本的,肯定可以更严格,但希望它能让你了解权限测试的基本概念。

假设我们有一个CMS,有两个默认角色:"管理员 "和 "编辑"。我们还假设,我们的系统不允许直接给用户分配权限。相反,权限只能被分配给角色,然后用户可以被分配到其中一个角色。

比方说,默认情况下,"管理员 "角色拥有创建/更新/删除用户和创建/更新/删除博客文章的权限。比方说,"编辑 "角色只有创建/更新/删除博客文章的权限。

现在,让我们来看看这个基本的示例路由和控制器,我们可以去创建一个新的用户。

Route::get(
    '/users/create',
    [\App\Http\Controllers\UserController::class, 'create']
)->middleware('can:create-users');

namespace App\Http\Controllers;

use Illuminate\View\View;

class UserController extends Controller
{
    // ...

    public function create(): View{
        return view('users.create');
    }

    // ...
}

正如你所看到的,我们已经在路由中添加了授权,所以只有拥有create-users 权限的用户才被允许访问。

现在,我们可以编写我们的测试了。

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    use RefreshDatabase;

    private User $user;

    private Role $role;

    protected function setUp()
    {
        parent::setUp();

        $this->user = User::factory()->create();

        $this->role = Role::create(['name' => 'custom-role']);
        $this->user->assignRole($this->role);

        $this->role->givePermissionTo('create-users');
    }

    /** @test */public function view_is_returned_if_the_user_has_permission()
    {
        $this->actingAs($this->user)
            ->get('/users/create')
            ->assertOk();
    }

    /** @test */public function access_is_denied_if_the_user_does_not_have_permission()
    {
        $this->role->revokePermissionTo('create-users');

        $this->actingAs($this->user)
            ->get('/users/create')
            ->assertForbidden();
    }
}

额外提示

如果你要自己创建权限,而不是让你的用户创建,那么将你的权限和角色名称存储为常量或枚举是相当有用的。例如,为了定义你的权限名称,你可以有一个这样的文件。

namespace App\Permissions;

class Permission
{
    public const CAN_CREATE_BLOG_POSTS = 'create-blog-posts';
    public const CAN_UPDATE_BLOG_POSTS = 'update-blog-posts';
    public const CAN_DELETE_BLOG_POSTS = 'delete-blog-posts';

    public const CAN_CREATE_USERS = 'create-users';
    public const CAN_UPDATE_USERS = 'update-users';
    public const CAN_DELETE_USERS = 'delete-users';
}

通过使用这样的文件, 可以更容易地避免任何可能导致任何意外bug的拼写错误。例如,让我们想象一下,我们有一个叫做create-blog-posts 的权限,我们有这样一行代码。

$user->can('create-blog-post');

如果你在审查拉动请求中的这段代码或自己写这段代码,我不会责怪你认为它是有效的。然而,我们在权限的结尾处省略了s!所以,为了避免这个问题,我们可以使用下面的方式。

use App\Permissions\Permission;

$user->can(Permission::CAN_CREATE_BLOG_POSTS);

现在, 我们对权限名称的正确性更有信心了。作为额外的奖励,如果你想在任何地方看到这个权限被使用,这也会变得非常容易,因为你的IDE(例如PHPStorm)应该能够检测到它在哪些文件中被使用。

其他软件包和方法

除了使用Spatie的Laravel Permission包,还有其他包可以用来为你的应用程序添加角色和权限。例如, 你可以使用BouncerLaratrust.

你可能会发现, 在你的一些应用程序中, 你需要更多的定制功能和灵活性, 而不是这些包所提供的.在这种情况下,你可能需要编写你自己的角色和权限实现。一个好的起点是使用Laravel的 "门 "和 "政策", 如前所述.

总结

希望这篇文章能让你了解如何使用Spatie的Laravel权限包为你的Laravel应用添加权限.它也应该让你了解到如何在PHPUnit中编写自动化测试,以测试你的权限设置是否正确。

作者:迪鲁宾

链接:https://juejin.cn/post/7109764810660642823