Skip to content

Laravel 权限实现

简介

Laravel 提供策略来定义应用程序的授权逻辑。这些策略建立了规则,规定哪些用户有权对应用程序的模型执行特定操作。在本指南中,我们将讨论使用数据库架构、特征和策略在 Laravel 应用程序中实现细粒度权限所需的步骤。

先决条件

在继续之前,请确保您已设置并运行了 Laravel 应用程序。您还应该熟悉 Laravel 迁移、模型和策略。

数据库架构

要在 Laravel 应用程序中实现权限,请创建一个包含角色、权限及其关系的数据库架构。以下示例演示如何使用 Laravel 迁移创建这些表:

bash
php artisan make:model Role -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * 运行迁移。
     */
    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * 回滚迁移。
     */
    public function down(): void
    {
        Schema::dropIfExists('roles');
    }
};
bash
php artisan make:model Permission -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * 运行迁移。
     */
    public function up(): void
    {
        Schema::create('permissions', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * 回滚迁移。
     */
    public function down(): void
    {
        Schema::dropIfExists('permissions');
    }
};
bash
php artisan make:model RolePermission -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * 运行迁移。
     */
    public function up(): void
    {
        Schema::create('role_permissions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
            $table->foreignId('permission_id')->constrained('permissions')->onDelete('cascade');
            $table->softDeletes();
            $table->timestamps();
        });
    }

    /**
     * 回滚迁移。
     */
    public function down(): void
    {
        Schema::dropIfExists('role_permissions');
    }
};
bash
php artisan make:model UserRole -m
php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * 运行迁移。
     */
    public function up(): void
    {
        Schema::create('user_roles', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
            $table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * 回滚迁移。
     */
    public function down(): void
    {
        Schema::dropIfExists('user_roles');
    }
};

在这个例子中,我们创建了角色、权限及其关系的表。roles 表包括 ID、名称、描述和时间戳。permissions 表包括 ID、名称、描述和时间戳。role_permissions 表包含 ID、role_idpermission_id 和时间戳。role_idpermission_id 列是外键,分别引用 rolespermissions 表中的 id 列。

模型架构

为了在与 RolePermissionUser 模型交互时提供类型提示并提高代码可读性,我们创建了相应的 <Model>Schema 类。这些类定义了表示相应数据库表中列名的常量。

例如,这是 PermissionSchema 类:

php
<?php

namespace App\Schemas;

class PermissionSchema
{
    const id = 'id';
    const name = 'name';
    const description = 'description';
    const created_at = 'created_at';
    const updated_at = 'updated_at';
    const deleted_at = 'deleted_at';
}

在上面的例子中,我们为权限表中的每一列定义了常量。然后,我们可以使用这些常量来提供类型提示,并在与 Permission 模型及其相关数据交互时提高代码可读性。

我们稍后将使用这些架构来定义与模型交互时列的名称。例如,在我们稍后创建的 HasPermissions 特征中,我们使用 PermissionSchema::name 常量来引用权限表中的 name 列:

php
use App\Schemas\PermissionSchema;

// ...

public function hasPermission(string $permission): bool
{
    return $this->roles->some(function ($role) use ($permission) {
        return $role->permissions->contains(PermissionSchema::name, $permission);
    });
}

通过使用 PermissionSchema::name 而不是原始字符串 "name",我们使我们的代码更具可读性,并且更不容易出错。此外,如果我们 ever 需要更改权限表中 name 列的名称,我们只需更新 PermissionSchema::name 常量以反映新名称,我们代码中对该常量的所有引用都会自动更新。

通过这种方式使用模型架构,我们可以使我们的代码更易于维护,并且随着时间的推移更容易使用。

模型

要在 Laravel 应用程序中实现细粒度权限,请创建四个模型:PermissionRoleRolePermissionUserRole。这些模型与存储角色、权限和用户/角色关系信息的数据库表交互。

这是 Role 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property string $name
 * @property string|null $description
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class Role extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'user_roles')->withTimestamps();
    }

    public function permissions(): BelongsToMany
    {
        return $this->belongsToMany(Permission::class, 'role_permissions')->withTimestamps();
    }
}

在上面的 Role 模型示例中,我们使用 BelongsToMany 关系来定义 RoleUser 模型之间的关系,以及 RolePermission 模型之间的关系。

这是 Permission 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property string $name
 * @property string|null $description
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class Permission extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'role_permissions')->withTimestamps();
    }
}

在上面的 Permission 模型示例中,我们使用 BelongsToMany 关系来定义 PermissionRole 模型之间的关系。

这是 RolePermission 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property int $role_id
 * @property int $permission_id
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class RolePermission extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }

    public function permission(): BelongsTo
    {
        return $this->belongsTo(Permission::class);
    }
}

在上面的 RolePermission 模型示例中,我们使用 BelongsTo 关系来定义 RolePermissionRole 模型之间的关系,以及 RolePermissionPermission 模型之间的关系。

这是 UserRole 模型:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property int $user_id
 * @property int $role_id
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon $deleted_at
 */
class UserRole extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }
}

在上面的 UserRole 模型示例中,我们使用 BelongsTo 关系来定义 UserRoleUser 模型之间的关系,以及 UserRoleRole 模型之间的关系。

最后,这是 User 模型:

php
<?php

namespace App\Models;

use App\Traits\HasPermissions;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

/**
 * 代表系统中用户的用户模型。
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property \DateTime|null $email_verified_at
 * @property string $password
 * @property string|null $remember_token
 * @property int|null $contact_id
 * @property \DateTime $created_at
 * @property \DateTime $updated_at
 */
class User extends Authenticatable
{
    use HasFactory, HasPermissions, Notifiable;

    /**
     * 可批量赋值的属性。
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * 应该为序列化隐藏的属性。
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * 应该被转换的属性。
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'user_roles')->withTimestamps();
    }
}

在上面的 User 模型示例中,我们使用 BelongsToMany 关系来定义 UserRole 模型之间的关系。此外,我们使用各种特征和属性来处理身份验证、通知和权限。

权限检查特征

为了简化策略中的权限检查,创建一个特征,为使用它的类添加 hasPermission() 方法。以下示例演示如何创建此特征:

php
<?php

namespace App\Traits;

use App\Schemas\PermissionSchema;

trait HasPermissions
{
    public function hasPermission(string $permission): bool
    {
        return $this->roles->some(function ($role) use ($permission) {
            return $role->permissions->contains(PermissionSchema::name, $permission);
        });
    }
}

此方法简化了检查用户是否具有特定权限的过程。

hasPermission 方法接受一个字符串参数 $permission,并返回一个布尔值。它通过检查与用户关联的所有角色来检查当前用户是否具有指定的权限。

如果没有角色具有所需的 $permissionsome 函数返回 false,表示用户没有必要的权限。

通过将此特征合并到您的 User 模型中,您可以轻松验证用户是否具有特定权限。

使用策略实现细粒度权限

要使用 User 模型应用细粒度权限,请为您想要控制访问的每个模型创建一个策略类。在此示例中,我们将创建一个 PostPolicy 来管理对 Post 模型的访问。

首先,创建 PostPolicy 类:

php
namespace App\Policies;

use App\Models\User;
use App\Models\Post;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

    public function viewAny(User $user)
    {
        return $user->hasPermission('viewAny-posts');
    }

    public function view(User $user, Post $post)
    {
        return $user->hasPermission('view-posts') && $post->user_id === $user->id;
    }

    public function create(User $user)
    {
        return $user->hasPermission('create-posts');
    }

    public function update(User $user, Post $post)
    {
        return $user->hasPermission('update-posts') && $post->user_id === $user->id;
    }

    public function delete(User $user, Post $post)
    {
        return $user->hasPermission('delete-posts') && $post->user_id === $user->id;
    }
}

请注意,我们利用了添加到 User 模型中的 hasPermission 特征。

接下来,在 AuthServiceProvider 中注册策略。以下是注册 PostPolicy 的示例:

php
namespace App\Providers;

use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    public function boot()
    {
        $this->registerPolicies();
    }
}

在此示例中,我们为 Post 模型注册了 PostPolicy。这指示 Laravel 使用 PostPolicy 来控制对 Post 模型实例的访问。

现在,您可以在控制器中使用此策略来授权操作。以下是在控制器中使用 PostPolicy 的示例:

php
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function edit(Post $post)
    {
        $this->authorize('update', $post);

        return view('posts.edit', compact('post'));
    }

    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        // 更新帖子...
    }
}

在此示例中,我们使用 authorize() 方法来验证经过身份验证的用户是否有权更新给定的帖子,使用 PostPolicyupdate 方法。如果用户获得授权,我们允许他们编辑或更新帖子。如果没有,我们返回 HTTP 403 Forbidden 错误。

通过使用策略来控制对模型的访问,您可以确保只有授权用户才能在您的应用程序中执行敏感操作。