
Админ-панель в Laravel 5 – миграции, модели, middleware и роуты
28.04.2018 16:27 | Laravel
Если говорить о сайтах, то практически всегда есть группа пользователей или, как минимум, один пользователь с особыми правами – админ. Давайте посмотрим, как можно ограничить доступ к админ-панели в Laravel.
Есть несколько вариантов реализации. Отличаются они только тем, будем ли мы использовать роли, и сколько ролей может быть у пользователя (одна или больше). Во всех случаях порядок действий одинаков:
- Редактируем или создаём необходимые миграции и выполняем их.
- Создаём модели и, если того требует ситуация, описываем отношения между ними.
- Создаём и регистрируем middleware.
- Настраиваем роуты.
Надо заметить, что отличия будут только в миграциях и моделях, а middleware и роуты будут абсолютно идентичными. Сначала я опишу различия, а потом перейдём к общей части.
Вариант 1. Дополнительное поле is_admin
Самая простая реализация. Если сайт небольшой, то другой и не нужно.
Миграции
Эта версия не подразумевает никаких дополнительных таблиц / моделей / отношений. Нужно всего лишь добавить дополнительное поле is_admin
в файл миграции пользователей. Миграция уже есть «в коробке», поэтому открываем файл project/app/database/migrations/XXXX_XX_XX_create_users_table.php
и редактируем. Код должен выглядеть так:
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->boolean('is_admin')->default(0);
$table->rememberToken();
$table->timestamps();
});
}
...
}
Теперь открываем консоль и переходим в корень проекта. После чего запускаем миграции:
$ php artisan migrate
Модель
Модель User
создавать не нужно, так как она также есть по умолчанию (project/app/User.php
). Но нужно выполнить два действия. Во-первых, для того, чтобы не возникало ошибок при массовом заполнении (mass assignments), добавим атрибут is_admin
в массив $fillable
. Во-вторых, добавим метод isAdmin()
, который будет возвращать true
или false
, в зависимости от того, является ли пользователь админом. Вот код модели:
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name', 'email', 'password', 'is_admin'
];
protected $hidden = [
'password', 'remember_token',
];
public function isAdmin()
{
return $this->is_admin;
}
}
Вариант 2. Роли. Отношение «один ко многим»
В данной версии пользователю присваивается некая роль. Например: зарегистрированный, автор, модератор, админ и т.д. При этом мы хотим, чтобы у каждого пользователя была только одна роль.
Миграции
Поскольку нам понадобится и таблица, и модель ролей, создадим их одной командой в консоли:
$ php artisan make:migration -m Role
Не будем делать чего-то слишком сложного и добавим в файл миграции всего два поля id и name. Другими словами, файл миграции должен выглядеть так:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateRolesTable extends Migration
{
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->tinyIncrements('id');
$table->string('name')->unique();
});
}
public function down()
{
Schema::dropIfExists('roles');
}
}
Также нужно добавить поле role_id
в таблицу пользователей, поэтому открываем соответствующий файл миграции и делаем это:
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->tinyInteger('role_id')->unsigned();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
$table->foreign('role_id')->references('id')->on('roles');
});
}
...
}
Важно: обратите внимание, что добавлен также и внешний ключ, который ссылается на таблицу ролей, поэтому миграция ролей должна предшествовать миграции пользователей, иначе при запуске команды в консоли получим ошибку. Т.е., просто измените дату в названии миграции ролей на более раннюю. После этого изменения создаём миграции:
$ php artisan migrate
Модели
Что нужно прописать в модели Role
? В массив $fillable
всего одно поле, которое можно заполнять, нужно указать, что отсутствуют даты создания и обновления ($timestamps
). Кроме этого, опишем отношения между с моделью User
в методе users()
, который указывает на то, что у одной роли может быть много пользователей:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\User;
class Role extends Model
{
protected $fillable = ['name'];
public $timestamps = false;
public function users()
{
return $this->hasMany(User::class);
}
}
В свою очередь, в модели пользователей допишем атрибут role_id
в массив $fillable
. Также определим и обратное отношение в методе role()
и снова напишем метод isAdmin()
с учётом того, что теперь используются роли:
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Role;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name', 'email', 'password', 'role_id',
];
protected $hidden = [
'password', 'remember_token',
];
public function role()
{
return $this->belongsTo(Role::class);
}
public function isAdmin()
{
return $this->role->name == 'admin';
}
}
Вариант 3. Роли. Многие ко многим
Отличие от предыдущего варианта в том, что у пользователя может быть несколько ролей.
Миграции
Что до миграций – у пользователей не будет никаких дополнительных полей (т.е. после установки Laravel миграцию пользователей менять не нужно), но нам всё так же понадобится таблица ролей плюс промежуточная таблица role_user
. Внешние ключи будет содержать только промежуточная таблица, т.е. она должна быть создана после пользователей и ролей.
Миграция ролей точно такая же, как в предыдущем варианте, поэтому перейдём сразу к промежуточной role_user
. В этом случае модель не понадобится, поэтому создаём только миграцию командой:
$ php artisan make:migration create_role_user_table --create=role_user
Открываем файл и вставляем следующий код:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateRoleUserTable extends Migration
{
public function up()
{
Schema::create('role_user', function (Blueprint $table) {
$table->tinyInteger('role_id')->unsigned();
$table->integer('user_id')->unsigned();
$table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->primary(['user_id', 'role_id']);
});
}
public function down()
{
Schema::dropIfExists('role_user');
}
}
После этого вновь запускаем создание таблиц в базе данных:
$ php artisan migrate
Модели
В целом модели сходны со вторым вариантом, но теперь в обеих таблицах будем использовать belongsToMany
, и, в модели пользователей метод isAdmin()
снова будет несколько иным. Модель роли:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\User;
class Role extends Model
{
protected $fillable = ['name'];
public $timestamps = false;
public function users()
{
return $this->belongsToMany(User::class);
}
}
Модель пользователя:
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Role;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
public function roles()
{
return $this->belongsToMany(Role::class);
}
public function isAdmin()
{
return (boolean)$this->roles->where('name', 'admin')->count();
}
}
Обратите внимание, что в моделях не упоминается промежуточная таблица. Всё потому, что мы следуем соглашении об именовании, и Laravel сам понимает, что и где ему искать. При этом, никто не запрещает называть поля и таблицы как захочется, в этом случае просто нужно указать дополнительные аргументы (названия промежуточной таблицы и ключей):
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
К слову, нелишним будет почитать документацию, поскольку отношение «многие ко многим» рассматривается как раз на примере пользователей и ролей.
Заполнение таблицы ролей
Если вы решили использовать второй или третий варианты, то перед созданием пользователей надо в таблице создать несколько ролей. Опять-таки, сделать это можно по-разному – использовать seeder или tinker, или попросту создать записи прямо в БД. Сейчас это не столь важно, так как речь в данном посте не об этом. Используем консоль и tinker:
$ php artisan tinker
>>> App\Role::create(['name' => 'admin']);
>>> App\Role::create(['name' => 'registered']);
Т.е., в итоге в таблице ролей у нас есть две записи.
Middleware
Зачем нужен middleware? Это некий слой, который будет выполнен, прежде чем выполнится запрос (или иные действия). В нашем случае middleware одинаков для всех вышеописанных вариантов и будет проверять, является ли пользователь админом. Если да – управление передаётся дальше, если нет – перенаправляет пользователя туда, куда мы скажем.
Создаём middleware командой в консоли:
$ php artisan make:middleware CheckIfAdmin
Путь к только что созданному файлу – project/app/Http/Middleware/CheckIfAdmin.php
. Открываем его и вставляем следующий код:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class CheckIfAdmin
{
public function handle($request, Closure $next)
{
if (Auth::check() && Auth::user()->isAdmin()) {
return $next($request);
}
return redirect('admin/login');
}
}
Думаю, код и так прекрасно читается, но на всякий случай уточню – мы проверяем, авторизован ли пользователь и если да, то является ли он админом. Если оба условия верны, как и было обещано, выполняется HTTP-запрос, если нет – отправляем пользователя на страницу логина админ-панели.
Замечу, что вовсе не обязательно использовать фасад Auth
или хелпер auth()
, поскольку Laravel предоставляет данные о пользователе и в реквесте. Кроме того, можно немного изменить логику. Например, если пользователь не авторизован, будем отправлять его на страницу логина админ-панели. Если же он авторизован, но не является админом – отправим его на главную страницу сайта. Т.е. код будет выглядеть так:
<?php
namespace App\Http\Middleware;
use Closure;
class CheckIfAdmin
{
public function handle($request, Closure $next)
{
$user = $request->user();
if (!isset($user)) {
return redirect('admin/login');
}
if (!$user->isAdmin()) {
return redirect('/');
}
return $next($request);
}
}
Какой вариант выбрать – решать вам. Сути это не меняет и оба будут прекрасно работать. Теперь надо зарегистрировать наш middleware. Для этого открываем файл project/app/Http/Kernel.php
, находим массив $routedMiddleware
и добавляем наш класс:
class Kernel extends HttpKernel
{
...
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'admin' => \App\Http\Middleware\CheckIfAdmin::class,
];
}
Роуты
Прежде чем приступить к роутам, подумаем вот о чём: фактически приложение будет включать два подприложения – непосредственно сам сайт и админ-панель. А значит, есть смысл разбить и роуты на две группы. Также можно сгруппировать и контроллеры – в папке app/Http/Controllers
создадим две директории. В первой – Admin – будут контроллеры для админ-панели (например, DashboardController), а во второй – Site – контроллеры для фронтенда (например, HomeController). При этом заметим, что во всех адресах админки после имени сайта будет присутствовать префикс admin
. Т.е. есть смысл сгруппировать роуты по префиксам и неймспейсам. Также при каждом запросе проверять, является ли пользователь админом. За исключением одного случая – входа в админку, т.е. когда пользователь ещё не авторизован. С учётом всего сказанного напишем такие роуты (в файле project/app/routes/web.php
):
// Admin routes
Route::group(['prefix' => 'admin', 'namespace' => 'Admin'], function() {
Route::get('login', 'LoginController@index');
Route::post('login', 'LoginController@login')->name('admin.login');
Route::group(['middleware' => 'admin'], function() {
Route::get('/', 'DashboardController')->name('admin.dashboard');
});
});
// Site routes
Route::group(['namespace' => 'Site'], function() {
Route::get('/', 'HomeController@index')->name('homepage');
});
Вот собственно и всё. Конечно, можно было бы привести ещё парочку контроллеров и представлений, но, полагаю, с этим не так уж и сложно разобраться. Главное - есть основа, которую можно использовать при создании любых сайтов.