Руководства по Ролям и Правам в Laravel

Роли и Права в Laravel

Роли (Roles) и Права (Permissions) в Laravel являются наиболее важной частью любого Laravel приложения, где необходимо ограничивать варианты его использование. Если вы погуглите Laravel Roles and Permissions, то найдете несколько пакетов для добавления подобного функционала. Вы можете установить их в свое приложение через Composer и, после небольшой настройки, их можно уже использовать.

В большинстве случаев эти пакеты поставляются с дополнительным функционалом, который вам вообще не нужен. Что если, вам просто хотите систему управления ролями и правами под ваши конкретные нужды. Когда речь заходит о функционале уже доступным через Composer, многие называют это «изобретение велосипеда».

По моему мнению, если мне нужно написать какой-то простой код, то зачем мне раздувать список зависимостей проекта? Кроме того, самостоятельно внедряя все это, вы в будущем получите полный контроль над кодом при необходимости новых изменений.

В этой статье я расскажу вам, как вы можете самостоятельно пошагово реализовать Роли и Права в Laravel.

Настройка приложения

Начнем урок с создания нового приложения Laravel с помощью приведенной ниже команды composer.

composer create-project laravel/laravel RolesAndPermissions

После создания приложения, перейдите в папку RolesAndPermissions и настройте учетные данные базы данных в файле .env.

Создание каркаса аутентификации

В Laravel 6 команда make:auth и весь фронтенд были перемещены в автономный пакет laravel/ui. Мы установим его, выполнив следующую команду.

composer require laravel/ui

Теперь мы сгенерируем дефолтный каркас аутентификации из предыдущих версиях Laravel.

php artisan ui vue --auth

После чего, выполните следующую команду, чтобы скомпилировать и сгенерировать необходимые ресурсы для фронтенд шаблонов.

npm install && npm run dev

Запустить встроенный сервер можно с помощью команды php artisan serve. Он будет работать по адресу localhost:8000.

Генерация Моделей и Миграций

В этом разделе мы создадим две новые модели под названием Role и Permission вместе с их миграциями. Для этого мы запустим две нижеприведенные команды в терминале командной строки.

php artisan make:model Role -m

php artisan make:model Permission -m

Флаг -m позволит генерировать миграцию вместе с моделью. Откроем файлы миграции и обновим их в соответствии со структурой таблицы базы данных, которую мы хотим реализовать.

Откройте файл миграции Роли в папке database/migrations и вставьте в него.

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

class CreateRolesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('slug');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('roles');
    }
}

Мы просто добавили поля name и slug. Теперь откройте файл миграции Прав и вставьте в него.

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

class CreatePermissionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('permissions', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('slug');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('permissions');
    }
}

Так же, как и в предыдущей миграции, мы добавили два поля name и slug. Всё просто.

Добавление необходимых сводных таблиц

Перед миграцией созданных таблиц, давайте определим некоторые отношения, которые мы хотели бы реализовать для Ролей и Прав.

  1. Пользователь может иметь Права
  2. Пользователь может иметь Роли
  3. Роль может иметь Права

Для этих трех отношений нам нужно добавить три сводные таблицы, чтобы создать отношение «Многие ко Многим» между моделями User, Role и Permission.

Давайте создадим миграции для этих сводных таблиц. Сначала мы создадим таблицу для связи между Пользователем и Правами.

php artisan make:migration create_users_permissions_table

Откройте только что созданный файл create_users_permissions_table и скопируйте в него.

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

class CreateUsersPermissionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users_permissions', function (Blueprint $table) {
            $table->unsignedBigInteger('user_id');
            $table->unsignedBigInteger('permission_id');

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');

            $table->primary(['user_id','permission_id']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users_permissions');
    }
}

В этой миграции мы определили два столбца user_id и permission_id с внешними ключами в соответствующих таблицах. Плюс, мы определили первичные ключи для этих двух полей.

Далее мы создадим ссылку между таблицами Пользователей и Ролей.

php artisan make:migration create_users_roles_table

Откройте созданный файл миграции create_users_roles_table и скопируйте в него.

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

class CreateUsersRolesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users_roles', function (Blueprint $table) {
            $table->unsignedBigInteger('user_id');
            $table->unsignedBigInteger('role_id');

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');

            $table->primary(['user_id','role_id']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users_roles');
    }
}

Аналогично предыдущей миграции, у нас также есть два поля user_id и role_id с внешними ключами в соответствующих таблицах и первичными ключами.

Теперь мы создадим сводную таблицу между Ролями и Правами.

php artisan make:migration create_roles_permissions_table

Откройте файл миграции create_roles_permissions_table и скопируйте в него.

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

class CreateRolesPermissionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('roles_permissions', function (Blueprint $table) {
            $table->unsignedBigInteger('role_id');
            $table->unsignedBigInteger('permission_id');

            $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
            $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');

            $table->primary(['role_id','permission_id']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('roles_permissions');
    }
}

В этой миграции мы определили два поля role_id и permission_id с внешним и первичным ключами.

Теперь у нас есть все необходимые таблицы базы данных, пора начать миграцию.

php artisan migrate

Если все прошло успешно, то вы увидите в вашей базе данных таблицы roles, permissions и три сводные таблицы.

Отношения Ролей и Прав

В этом разделе мы настроим отношения Ролей и Прав. Для Пользователей мы добавим отношения в следующем разделе. Откройте файл модели Role.php и добавьте отношение belongsToMany.

class Role extends Model
{
    public function permissions()
    {
        return $this->belongsToMany(Permission::class,'roles_permissions');
    }
}

Откройте файл модели Permission.php и скопируйте в него.

class Permission extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class,'roles_permissions');
    }
}

Так мы определили отношения «Многие ко Многим» между Ролями и Правами.

Трейт HasRolesAndPermissions для модели User

Теперь займемся моделью User. Пользователь может иметь много Прав и много Ролей. То же самое наоборот, Роль может иметь много Пользователей, а Право может иметь много Пользователей. Поэтому нам нужно создать отношение «Многие ко Многим» в модели User.

Для чистоты кода я создам эти отношения в трейте, а затем использую его в модели User. Мы также можем использовать этот трейт позже, если добавим в наше приложение какую-либо модель, требующую Роли и Права.

В папке app создайте новую папку и назовите ее «Traits». Создайте в ней файл и назовите его HasRolesAndPermissions.php.

Скопируйте в него следующий код.

namespace App\Traits;

use App\Models\Role;
use App\Models\Permission;

trait HasRolesAndPermissions
{
    /**
     * @return mixed
     */
    public function roles()
    {
        return $this->belongsToMany(Role::class,'users_roles');
    }

    /**
     * @return mixed
     */
    public function permissions()
    {
        return $this->belongsToMany(Permission::class,'users_permissions');
    }
}

Мы задали отношения roles и permissions в соответствующих моделях. Теперь для того, чтобы использовать этот трейт в вашей модели User, откройте файл User.php и добавьте в него использование трейта HasRolesAndPermissions:

use App\Traits\HasRolesAndPermissions;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable, HasRolesAndPermissions; // Наш новый трейт

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

Пользователь hasRole

Чтобы проверить, есть ли у текущего залогиненного Пользователя Роль, мы добавим новую функцию в трейт HasRolesAndPermissions. Откройте этот файл трейта и добавьте в него эту функцию.

/**
 * @param mixed ...$roles
 * @return bool
 */
public function hasRole(... $roles ) {
    foreach ($roles as $role) {
        if ($this->roles->contains('slug', $role)) {
            return true;
        }
    }
    return false;
}

В функцию мы передаем массив $roles и проверяем в цикле, содержат ли роли текущего пользователя заданную роль.

Пользователь hasPermission

Для проверки прав доступа текущего пользователя, мы добавим два нижеприведенных метода в наш трейт HasRolesAndPermissions.

/**
 * @param $permission
 * @return bool
 */
public function hasPermission($permission)
{
    return (bool) $this->permissions->where('slug', $permission)->count();
}

/**
 * @param $permission
 * @return bool
 */
public function hasPermissionTo($permission)
{
    return $this->hasPermission($permission);
}

Метод проверяет, содержат ли права пользователя заданное право, если да, то тогда он вернет true, а иначе false.

Пользователь hasPermissionThroughRole

Как мы знаем, у нас между Ролями и Правами есть отношение «Многие ко Многим». Это позволяет нам проверять, есть ли у Пользователя Права через его Роль. Чтобы это реализовать, мы добавим новую функцию в наш трейт HasRolesAndPermissions.

/**
 * @param $permission
 * @return bool
 */
public function hasPermissionThroughRole($permission)
{
    foreach ($permission->roles as $role){
        if($this->roles->contains($role)) {
            return true;
        }
    }
    return false;
}

Эта функция проверяет, привязана ли Роль с Правами к Пользователю. Метод hasPermissionTo() проверит эти два условия.

Обновите метод hasPermissionTo, как показано ниже.

/**
 * @param $permission
 * @return bool
 */
public function hasPermissionTo($permission)
{
   return $this->hasPermissionThroughRole($permission) || $this->hasPermission($permission->slug);
}

Теперь у нас есть метод, который будет проверять, есть ли у Пользователя Права напрямую или через Роль. Позже мы будем использовать этот метод для добавления кастомной blade-директивы.

Выдача Прав

Предположим, что мы хотим прикрепить некоторые Права к текущему Пользователю. Для этого мы добавим новый метод в трейт HasRolesAndPermissions.

/**
 * @param array $permissions
 * @return mixed
 */
public function getAllPermissions(array $permissions)
{
    return Permission::whereIn('slug',$permissions)->get();
}

/**
 * @param mixed ...$permissions
 * @return $this
 */
public function givePermissionsTo(... $permissions)
{
    $permissions = $this->getAllPermissions($permissions);
    if($permissions === null) {
        return $this;
    }
    $this->permissions()->saveMany($permissions);
    return $this;
}

Первый метод получает все Права на основе переданного массива. Во второй функции мы передаем Права в виде массива и получаем все Права из базы данных на основе массива.

Далее мы используем метод permissions() для вызова метода saveMany(), чтобы сохранить разрешения для текущего пользователя.

Удаление Прав

Чтобы удалить Права Пользователя, мы передаем Права методу deletePermissions() и удаляем все прикрепленные Права с помощью метода detach().

/**
 * @param mixed ...$permissions
 * @return $this
 */
public function deletePermissions(... $permissions )
{
    $permissions = $this->getAllPermissions($permissions);
    $this->permissions()->detach($permissions);
    return $this;
}

/**
 * @param mixed ...$permissions
 * @return HasRolesAndPermissions
 */
public function refreshPermissions(... $permissions )
{
    $this->permissions()->detach();
    return $this->givePermissionsTo($permissions);
}

Второй метод фактически удаляет все Права Пользователя, а затем переназначает предоставленные для него Права.

Добавление сидеров

До сих пор мы реализовали основные Роли и Права в нашем приложении Laravel, ничего не тестирую. Для быстрого тестирования мы сделаем несколько классов Seed, которые добавят в ваши таблицы несколько фиктивных данных.

php artisan make:seeder PermissionSeeder

php artisan make:seeder RoleSeeder

php artisan make:seeder UserSeeder

Откройте класс RoleSeeder и скопируйте в него.

namespace Database\Seeders;

use App\Models\Role;
use Illuminate\Database\Seeder;

class RoleSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $manager = new Role();
        $manager->name = 'Project Manager';
        $manager->slug = 'project-manager';
        $manager->save();

        $developer = new Role();
        $developer->name = 'Web Developer';
        $developer->slug = 'web-developer';
        $developer->save();
    }
}

Откройте класс PermissionSeeder и скопируйте в него.

namespace Database\Seeders;

use App\Models\Permission;
use Illuminate\Database\Seeder;

class PermissionSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $manageUser = new Permission();
        $manageUser->name = 'Manage users';
        $manageUser->slug = 'manage-users';
        $manageUser->save();

        $createTasks = new Permission();
        $createTasks->name = 'Create Tasks';
        $createTasks->slug = 'create-tasks';
        $createTasks->save();
    }
}

Далее, в классе UserSeeder мы создадим несколько Пользователей и добавим к ним Роли и Права.

namespace Database\Seeders;

use App\Models\Role;
use App\Models\User;
use App\Models\Permission;
use Illuminate\Database\Seeder;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $developer = Role::where('slug','web-developer')->first();
        $manager = Role::where('slug', 'project-manager')->first();
        $createTasks = Permission::where('slug','create-tasks')->first();
        $manageUsers = Permission::where('slug','manage-users')->first();

        $user1 = new User();
        $user1->name = 'Jhon Deo';
        $user1->email = 'jhon@deo.com';
        $user1->password = bcrypt('secret');
        $user1->save();
        $user1->roles()->attach($developer);
        $user1->permissions()->attach($createTasks);


        $user2 = new User();
        $user2->name = 'Mike Thomas';
        $user2->email = 'mike@thomas.com';
        $user2->password = bcrypt('secret');
        $user2->save();
        $user2->roles()->attach($manager);
        $user2->permissions()->attach($manageUsers);
    }
}

Теперь, когда у нас есть три готовых сида, давайте обновим класс DatabaseSeeder, как показано ниже.

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(RoleSeeder::class);
        $this->call(PermissionSeeder::class);
        $this->call(UserSeeder::class);
    }
}

Чтобы сохранить данные в базе данных, выполните в терминале следующую команду.

php artisan db:seed

Проверьте Права и Роли Пользователя, как показано ниже.

$user = App\Models\User::find(1);
dd($user->hasRole('web-developer')); //вернёт true
dd($user->hasRole('project-manager')); //вернёт false
dd($user->givePermissionsTo('manage-users')); //выдаём разрешение
dd($user->hasPermission('manage-users')); //вернёт true

Добавление кастомной blade-директивы для Ролей и Прав

В этом разделе мы создадим директиву, которой будем пользоваться в blade-шаблонах. Для начала мы создадим нового сервис-провайдера.

php artisan make:provider RolesServiceProvider

Не забудьте добавить RolesServiceProvider в список providers в файле config/app.php. Откроем свежесозданный RolesServiceProvider и обновим его с помощью приведенного ниже кода.

namespace App\Providers;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class RolesServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        Blade::directive('role', function ($role){
            return "<?php if(auth()->check() && auth()->user()->hasRole({$role})): ?>";
        });

        Blade::directive('endrole', function ($role){
            return "<?php endif; ?>";
        });
    }
}

В сервис провайдере мы объявляем кастомную директиву, используя фасад Blade. В первой директиве мы проверяем, прошел ли Пользователь аутентификацию и имеет ли он заданную роль. Во второй директиве — закрываем оператор if.

В шаблонах мы можем использовать директиву следующим образом:

@role('project-manager')
 Project Manager Panel
@endrole 

@role('web-developer')
 Web Developer Panel
@endrole

Очень просто.

До сих пор мы использовали Роли в нашей директиве. Для Прав мы будем использовать директиву can, чтобы проверить, есть ли у Пользователя Право. Вместо использования $user->hasPermissionTo() мы будем использовать Gate::allows('manage-users').

Для достижения этой функциональности мы создадим нового сервис провайдера и назовем его PermissionServiceProvider.

php artisan make:provider PermissionServiceProvider

Не забудьте добавить PermissionServiceProvider в список providers в файле config/app.php. Откройте его и обновите его с помощью приведенного ниже кода.

namespace App\Providers;

use App\Models\Permission;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class PermissionServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        try {
            Permission::get()->map(function ($permission) {
                Gate::define($permission->slug, function ($user) use ($permission) {
                    return $user->hasPermissionTo($permission);
                });
            });
        } catch (\Exception $e) {
            report($e);
            return false;
        }
    }
}

Здесь мы сопоставляем все Права, определяем slug Права (в нашем случае) и проверяем, есть ли у Пользователя Право. Теперь вы можете проверить Права Пользователя, как показано ниже.

//вернёт true для текущего пользователя, если ему дано право управлять пользователями
Gate::allows('manage-users');

Добавление Мидлвара для Ролей и Прав

Мы можем создать специфичные Ролевые области в веб-приложении. Например, можно предоставить доступ для управления Пользователями только Менеджерам проекта. Для этого мы будем использовать Laravel мидлвары. Используя их, мы можем добавить дополнительный контроль над входящими запросами.

Чтобы создать мидлвар для Ролей, выполните команду ниже.

php artisan make:middleware RoleMiddleware

Откройте созданный класс RoleMiddleware и скопируйте в него.

namespace App\Http\Middleware;

use Closure;

class RoleMiddleware
{
    /**
     * Handle an incoming request.
     * @param $request
     * @param Closure $next
     * @param $role
     * @param null $permission
     * @return mixed
     */
    public function handle($request, Closure $next, $role, $permission = null)
    {
        if(!auth()->user()->hasRole($role)) {
            abort(404);
        }
        if($permission !== null && !auth()->user()->can($permission)) {
            abort(404);
        }
        return $next($request);
    }
}

В этом мидлваре мы проверяем, имеет ли текущий Пользователь заданную Роль/Право, и, если нет, то возвращаем страницу с ошибкой 404. Существует много возможностей использовать Роли и Права в мидлваре для управления входящими запросами, все зависит от требований вашего приложения.

Перед использованием этого мидлвара вы должны добавить его в файл App\Http\Kernel.php.

Обновите массив $routeMiddleware как показано ниже.

 /**
 * The application's route middleware.
 *
 * These middleware may be assigned to groups or used individually.
 *
 * @var array
 */
protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    'role'  =>  \App\Http\Middleware\RoleMiddleware::class, // наш мидлвар роли
];

Теперь вы можете использовать мидлвар, как показано ниже.

Route::group(['middleware' => 'role:web-developer'], function() {
   Route::get('/dashboard', function() {
      return 'Добро пожаловать, Веб-разработчик';
   });
});

Выводы

В этой статье мы рассмотрели, как легко можно создать функционал Ролей и Прав без использования какого-либо конкретного пакета.
Для реализации этой концепции существует масса вариантов, я лишь упросил её, чтобы новички в Laravel могли легко её понять.

Вы можете найти код этой статьи в репозитории Laravel Roles Permissions.

Update 22.05.2020: Исправлены ошибка с типами полей в миграциях.

Update 10.09.2020: Добавлены неймспейсы, исправлены сидеры, добавлены пояснения к созданию провайдеров.

Update 23.09.2020: Совместимость с Laravel 8: изменено расположение моделей. Исправлена ошибка Trying to get property 'slug' of non-object. Исправлено использование проверки прав.

Автор: Larashout
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.