Из этой статьи вы узнаете, как реализовать систему аутентификации на основе ролей пользователей в своем приложении с помощью Laravel Sanctum. Все исходники доступны на GitHub.
Требования
- Базовые знания PHP и Laravel
- Любой API-инструмент, например Postman, Insomnia, hoppscotch и т.д. Лично я использую Insomnia.
- Установленный PHP Composer
- Веб-сервер, например, Nginx, Apache2, Caddy и т.д.
Что такое Laravel Sanctum
Это пакет, созданный и поддерживаемый основной командой Laravel, который можно использовать для аутентификации по API-токену или SPA (одностраничное приложение) или даже для мобильных приложений.
В этой статье мы рассмотрим работу с API-токенами: выпуск токена, его аутентификацию и авторизацию. Прежде чем мы перейдем к деталям, было бы неплохо понять, почему мы вообще используем аутентификацию на основе токенов.
Аутентификацию на основе токенов
Обычно при базовой HTTP-аутентификации для подтверждения аутентификации передаются имя пользователя и пароль.
Преимущество этого подхода заключается в том, что он не имеет состояния, то есть на сервере не хранятся файлы cookie или идентификаторы сеансов, и поэтому нет необходимости в сложной настройке, HTTP-заголовок всё cделает за вас.
Недостатком такого подхода является то, что вам не только придется вводить пользователя и пароль каждый раз, когда нужно аутентифицировать себя, но и также нужно убедиться, что этот процесс происходит в полной безопасности по протоколу SSL, иначе есть риск подвергнуться «Атаке посредника», когда злоумышленник может просто перехватить данные для входа и творить с ними всё, что захочет.
Даже если вы используете SSL, то вам все равно придется аутентифицироваться с помощью имени пользователя и пароля для каждого защищенного ресурса. И это явно не идеальный вариант и всё равно нас приводит к токен-аутентификации.
При аутентификации на основе токена, для того чтобы вы могли подтвердить свою аутентификацию, вам нужно получить токен либо от системного администратора, либо сгенерировать его самостоятельно, если ваш аккаунт связан с системой — вспомните GitHub Personal Access Token .
Преимущество этого метода заключается в том, что пользователям не придется полагаться на логин и пароль и вводить их каждый раз, когда нужен доступ к определенным ресурсам, плюс вы, как эмитент, можете применять строгие правила относительно того, как токен может использоваться на вашей стороне, например ограничивать скорость запроса к API и права доступа, сроки действия токена и тому подобное. Laravel Sanctum позволяет все это реализовать из коробки, поэтому давайте перейдем к практике.
Шаг 1. Настройка Laravel и Sanctum
Создадим новый экземпляр Laravel-приложения, выполнив следующую команду в своем терминале:
composer create-project --prefer-dist laravel/laravel simpleblog
Вышеприведенная команда создаст папку simpleblog
и установит в неё новое Laravel-приложение. После установки обязательно перейдите в созданную папку.
Теперь установим Sanctum:
composer require laravel/sanctum
Добавим в файл .env
информацию о базе данных. Затем добавим поле role
в файл миграции User
. Функция up
должна выглядеть так:
public function up() { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->tinyInteger('role')->default(1); // <---- добавьте это $table->rememberToken(); $table->timestamps(); }); }
Опубликуем конфигурацию и миграции Sanctum:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate
Файл конфигурации Sanctum появится в каталоге config
, а в базе данных будет создана таблица personal_access_tokens
(в ней будут храниться токены) со следующими столбцами:
+----------------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | tokenable_type | varchar(255) | NO | MUL | NULL | | | tokenable_id | bigint(20) unsigned | NO | | NULL | | | name | varchar(255) | NO | | NULL | | | token | varchar(64) | NO | UNI | NULL | | | abilities | text | YES | | NULL | | | last_used_at | timestamp | YES | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +----------------+---------------------+------+-----+---------+----------------+
Вам не обязательно знать их наизусть — просто показываю как она выглядит.
Наконец, добавим трейт Laravel\Sanctum\HasApiTokens
в модель User
и заменим свойство $fillable
следующим:
use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasFactory, Notifiable, HasApiTokens; // --------------------------------↑ protected $fillable = [ 'name', 'email', 'password', 'role' ];
Теперь, для выпуска токенов, мы можем использовать метод createToken()
из трейта HasApiTokens
.
Создание API на основе ролей
Модель прав будет работать таким образом: у пользователя может быть определенная роль, с которой он может получать доступ к ресурсам, если ресурсы разрешают дают доступ этой роли. Это дает вам возможность контролировать, что пользователи могут делать с API.
Для простоты мы создадим роли: Admin (Администратор), Writer (Автор) и Subscriber (Подписчик). И следующие конечные точки REST API:
- GET
/posts
(список всех сообщений) — доступ есть только у Admin - GET
/posts/:id
(получить сообщение) — доступ Admin, Writer и Subscriber - POST
/posts
(добавить новое сообщение) — доступ Admin и Writer - PUT
/post/:id
(обновление сообщения) — доступ Admin и Writer - DELETE
/posts/:id
(удалить сообщение) — доступ Admin и Writer - POST
/users/writer
(добавить пользователя с ролью Writer) — доступ Admin - POST
/users/subscriber
(добавить пользователя с ролью Subscriber) — доступ Admin - DELETE
/user/:id
(удалить пользователя) — доступ Admin
Сделаем проект немного более реалистичным — первый зарегистрировавшийся пользователь получит роль Admin, а затем сможет создавать пользователей с меньшими правами: Writer и Subscriber.
Для начала создадим модель сообщений Post
:
php artisan make:model Post -m
и добавим функцию up
в миграцию posts_table
:
public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string('slug')->unique(); $table->longText('content'); $table->timestamps(); }); }
Очень простая таблица. Запустим миграцию:
php artisan migrate
Теперь перейдем к этапам создания API.
Шаг 2. Создаем каркас пользовательского интерфейса с помощью Laravel UI
Пакет Laravel UI обеспечивает минимально необходимый базис для создания приложения с аутентификацией и пользовательским интерфейсом. Например, пакет предоставит страницы входа и регистрации.
Примечание: вы также можете использовать пакет Laravel Breeze. Процесс использования похож.
Для начала установим Laravel UI через Composer:
composer require laravel/ui php artisan ui:auth
Я не буду утруждать себя оформлением страниц входа и регистрации, поскольку на данный момент это не важно.
Шаг 3. Регистрация только для пользователя, использующего мидлвар
Как я говорил ранее, первый зарегистрировавшийся пользователь получит роль Admin, поэтому пускаем на регистрацию только одного пользователя.
Мы можем сделать это с помощью Laravel Middleware, который создадим так:
php artisan make:middleware RestrictRegistrationToOneAdmin
Эта команда создаст мидлвар и поместит его в папку app/Htpp/Middleware
. В появившемся файле RestrictRegistrationToOneAdmin.php
добавьте в метод handle()
следующее:
public function handle(Request $request, Closure $next) { $user = DB::table('users')->select('role')->where('id',1)->first(); if ($user && (int)$user->role === 1){ // редиректим на главную страницу, если у нас уже есть пользователь с такой ролью return redirect("/"); } return $next($request); }
Поскольку администратор будет первым пользователем в таблице, то мы можем проверить, есть ли у него роль 1
(то есть роль администратора), если да, мы перенаправляем обратно на главную страницу. Это перенаправление сработает только если такой пользователь уже был создан.
Этот мидлвар добавим к регистрационному маршруту, но перед этим назначим ему ключ в файле в файле app/Http/Kernel.php
:
protected $routeMiddleware = [ // здесь другие мидлвары // ... 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'restrictothers' => RestrictRegistrationToOneAdmin::class // <— добавим это ];
После того как сделаем это, мы сможем использовать метод мидлвара в маршрутах. Откройте файл routes/web.php
и добавьте новый маршрут после Auth::routes()
:
use App\Http\Controllers\Auth\RegisterController; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; Auth::routes(); // Добавьте это ↓ Route::post('register', [RegisterController::class, 'register']) ->middleware('restrictothers'); // Страница создания токена Route::get('dashboard', function () { if(Auth::check() && Auth::user->role === 1){ return auth() ->user() ->createToken('auth_token', ['admin']) ->plainTextToken; } return redirect("/"); })->middleware('auth');
Как только вы зарегистрируетесь или войдете в систему как администратор, то будете перенаправлены на страницу панели управления. В замыкании мы проверяем, аутентифицирован ли пользователь и является ли он администратором. Если да, то создаем и возвращаем API-токен.
После того, как вы это добавили, нужно очистить кеш маршрута в консоли, выполнив следующую команду:
php artisan route:cache
Шаг 4. Выпуск и Отзыв токена администратора
Зайдите по адресу /register
. После успешной регистрации вам будет выдан токен. Скопируйте его себе.
Примечание: при каждом посещении будет создаваться новый токен.
Для удобства вы можете сделать кнопку, генерирующую токен.
Чтобы иметь возможность отзывать (удалять) токен, вы можете сделать в файле routes/web.php
следующее:
Route::get('clear/token', function () { if(Auth::check() && Auth::user()->role === 1) { Auth::user()->tokens()->delete(); } return 'Token Cleared'; })->middleware('auth');
При использовании маршрута clear/token
и при условии, что вы администратор, токен будет удалён. Опять таки, это всего лишь пример. Вы можете создать кнопку для удаления токенов.
Шаг 5: Создание конечной точки API и права на неё
Откройте файл routes/api.php
и добавьте следующие конечные точки:
use App\Http\Controllers\ControllerExample; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::group(['middleware' => 'auth:sanctum'], function() { // список всех сообщений Route::get('posts', [ControllerExample::class, 'post']); // получить сообщение Route::get('posts/{id}', [ControllerExample::class, 'singlePost']); // добавить сообщение Route::post('posts', [ControllerExample::class, 'createPost']); // обновить сообщение Route::put('posts/{id}', [ControllerExample::class, 'updatePost']); // удалить сообщение Route::delete('posts/{id}', [ControllerExample::class, 'deletePost']); // добавить нового пользователя с ролью Writer Route::post('users/writer', [ControllerExample::class, 'createWriter']); // добавить нового пользователя с Subscriber Route::post( 'users/subscriber', [ControllerExample::class, 'createSubscriber'] ); // удалить пользователя Route::delete('users/{id}', [ControllerExample::class, 'deleteUser']); });
Группы маршрутов позволяют использовать атрибуты на несколько маршрутов, без необходимости задавать их персонально для каждого.
В нашем случае мы используем малдвар auth: sanctum
для ограничения использования только аутентифицированными пользователями.
Аутентификация не означает, что пользователь авторизован. Аутентификация — это гарантия того, что пользователь аутентифицирован с помощью Sanctum-гварда. И уже после этого происходит авторизация — проверка имеет ли пользователь право на доступ к ресурсу.
Мы охватим все ресурсы с помощью возможностей Sanctum и определим авторизирован ли пользователь для доступа к ресурсам.
Шаг 6: Создание методов API-контроллера
Перед созданием методов давайте сначала создадим трейт AiHelpers.php
для общих функций. Создаем папку с Library
в каталоге app/Http
и поместим туда файл со следующими методами:
namespace App\Http\Library; use Illuminate\Http\JsonResponse; trait ApiHelpers { protected function isAdmin($user): bool { if (!empty($user)) { return $user->tokenCan('admin'); } return false; } protected function isWriter($user): bool { if (!empty($user)) { return $user->tokenCan('writer'); } return false; } protected function isSubscriber($user): bool { if (!empty($user)) { return $user->tokenCan('subscriber'); } return false; } protected function onSuccess($data, string $message = '', int $code = 200): JsonResponse { return response()->json([ 'status' => $code, 'message' => $message, 'data' => $data, ], $code); } protected function onError(int $code, string $message = ''): JsonResponse { return response()->json([ 'status' => $code, 'message' => $message, ], $code); } protected function postValidationRules(): array { return [ 'title' => 'required|string', 'content' => 'required|string', ]; } protected function userValidatedRules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]; } }
Все методы в вышеприведенном трейте в принципе понятны, однако в isAdmin
, isWriter
и isSubscriber
я использовал метод tokenCan
— это метод Sanctum, который проверяет, имеет ли токен указанную роль, в этом случае он возращает true
.
Мы задали роль admin
при создании пользователя-администратора, мы также должны задавать роль writer
при регистрации пользователей-писателей и роль subscriber
при регистрации пользователей-подписчиков.
В app/Http/Controllers/ControllerExample.php
добавим следующее:
namespace App\Http\Controllers; use App\Http\Library\ApiHelpers; use App\Models\Post; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Http\JsonResponse; use Illuminate\Support\Str; class ControllerExample extends Controller { use ApiHelpers; // <---- Использование трейта apiHelpers public function post(Request $request): JsonResponse { if ($this->isAdmin($request->user())) { $post = DB::table('posts')->get(); return $this->onSuccess($post, 'Post Retrieved'); } return $this->onError(401, 'Unauthorized Access'); } public function singlePost(Request $request, $id): JsonResponse { $user = $request->user(); if ($this->isAdmin($user) || $this->isWriter($user) || $this->isSubscriber($user)) { $post = DB::table('posts')->where('id', $id)->first(); if (!empty($post)) { return $this->onSuccess($post, 'Post Retrieved'); } return $this->onError(404, 'Post Not Found'); } return $this->onError(401, 'Unauthorized Access'); } public function createPost(Request $request): JsonResponse { $user = $request->user(); if ($this->isAdmin($user) || $this->isWriter($user)) { $validator = Validator::make($request->all(), $this->postValidationRules()); if ($validator->passes()) { // Создание нового сообщения $post = new Post(); $post->title = $request->input('title'); $post->slug = Str::slug($request->input('title')); $post->content = $request->input('content'); $post->save(); return $this->onSuccess($post, 'Post Created'); } return $this->onError(400, $validator->errors()); } return $this->onError(401, 'Unauthorized Access'); } public function updatePost(Request $request, $id): JsonResponse { $user = $request->user(); if ($this->isAdmin($user) || $this->isWriter($user)) { $validator = Validator::make($request->all(), $this->postValidationRules()); if ($validator->passes()) { // Обновление сообщения $post = Post::find($id); $post->title = $request->input('title'); $post->content = $request->input('content'); $post->save(); return $this->onSuccess($post, 'Post Updated'); } return $this->onError(400, $validator->errors()); } return $this->onError(401, 'Unauthorized Access'); } public function deletePost(Request $request, $id): JsonResponse { $user = $request->user(); if ($this->isAdmin($user) || $this->isWriter($user)) { $post = Post::find($id); // Найдем id сообщения $post->delete(); // Удаляем указанное сообщение if (!empty($post)) { return $this->onSuccess($post, 'Post Deleted'); } return $this->onError(404, 'Post Not Found'); } return $this->onError(401, 'Unauthorized Access'); } public function createWriter(Request $request): JsonResponse { $user = $request->user(); if ($this->isAdmin($user)) { $validator = Validator::make($request->all(), $this->userValidatedRules()); if ($validator->passes()) { // Создаем нового Автора User::create([ 'name' => $request->input('name'), 'email' => $request->input('email'), 'role' => 2, 'password' => Hash::make($request->input('password')), ]); $writerToken = $user->createToken('auth_token', ['writer'])->plainTextToken; return $this->onSuccess($writerToken, 'User Created With Writer Privilege'); } return $this->onError(400, $validator->errors()); } return $this->onError(401, 'Unauthorized Access'); } public function createSubscriber(Request $request): JsonResponse { $user = $request->user(); if ($this->isAdmin($user)) { $validator = Validator::make($request->all(), $this->userValidatedRules()); if ($validator->passes()) { // Создаем нового Подписчика User::create([ 'name' => $request->input('name'), 'email' => $request->input('email'), 'role' => 3, 'password' => Hash::make($request->input('password')), ]); $writerToken = $user->createToken('auth_token', ['subscriber'])->plainTextToken; return $this->onSuccess($writerToken, 'User Created With Subscriber Privilege'); } return $this->onError(400, $validator->errors()); } return $this->onError(401, 'Unauthorized Access'); } public function deleteUser(Request $request, $id): JsonResponse { $user = $request->user(); if ($this->isAdmin($user)) { $user = User::find($id); // Найдем id пользователя if ($user->role !== 1) { $user->delete(); // Удалим указанного пользователя if (!empty($user)) { return $this->onSuccess('', 'User Deleted'); } return $this->onError(404, 'User Not Found'); } } return $this->onError(401, 'Unauthorized Access'); } }
Объясню, что делает каждый метод:
post(Request $request)
Проверяет, является ли пользователь администратором. Если да, то выводим все сообщения. В противном случае возвращаем ошибку авторизации.singlePost(Request $request, $id)
Проверяет, является ли пользователь администратором, автором или подписчиком. Если да, то показываем запрошенное сообщение. Если сообщение не найдено, то возвращаем ошибку 404.createPost(Request $request)
Возможность создать сообщение есть только у Администраторов и Авторов, поэтому проверяем является ли аутентифицированный пользователь одним из таковых. Если да, мы проводим валидацию данных и в случае успеха создаем новое сообщение. Если же нет, то возвращаем ошибку. Если пользователь не имеет необходимой роли, то возращаем ошибку авторизации.updatePost(Request $request, $id)
Также как в методеcreatePost
, за исключением того, что мы просто обновляем данные указанного сообщения.deletePost(Request $request, $id)
Также как в методеcreatePost
, за исключением того, что мы удаляем указанное сообщение.createWriter(Request $request)
Возможность создавать Авторов есть только у Администратора, поэтому проверяем является ли аутентифицированный пользователь таковым. Если да, то создаем нового пользователя с ролью2
(в нашем случае это значит Автор). Самое главное, что как только пользователь создан, мы возвращаем токен и отмечаем его как Авторский (Writer).createSubscriber(Request $request)
Подобен методуcreateWriter
, за исключением того, что на этот раз мы отмечаем его как Подписочный (subscriber).deleteUser(Request $request, $id)
Сначала проверяется, авторизован ли пользователь как администратор. Страхуемся от удаления пользователя с ролью администратора (вы можете это изменить). Если всё в порядке, то удаляем пользователя.
Теперь можем перейти к тестированию.
Шаг 7. Тестирование API в Insomnia
Не обязательно использовать Insomnia, тестируйте сервисом, который вам нравится.
Создание нового сообщения
Начните с создания нового POST
-запроса (кликните на значок плюса или используйте хоткей Ctrl+N) и задайте ему имя.
В текстовом поле рядом со словом POST введите нужную конечную точку API. Поскольку мы создаем сообщение, то пишем: /api/posts
В выпадающем меню Auth выберите Bearer Token, и скопируйте туда токен администратора:
Теперь выберите вкладку Body и в выпадающем меню выберите JSON:
Создадим сообщение, используя ключи title
и content
, например:
{ "title":"This is a new post title", "content":"This is an new body" }
Нажмите кнопку Send для отправки сообщения. Вы должны получить от своего сервера данные созданного сообщения, которые появятся на правой панели.
Зеленый прямоугольник с надписью «200 OK» означает, что ваш запрос вернулся со статусом 200
, то есть он был успешным. Попробуйте создать поэксперементировать с отправкой других сообщений.
Обновление сообщения
Чтобы обновить сообщение, измените метод с POST
на PUT
, укажите идентификатор сообщения, которое вы хотите обновить, например, первое сообщение, которое мы только что создали. Соответственно наша конечная точка: /api/posts/1
Затем вносим изменения в тело JSON:
Получение сообщения
Чтобы получить сообщение, измените метод на GET
, укажите идентификатор сообщения, которое вы хотите получить. Например, для сообщения с идентификатором 1
, нужно использовать конечную точку: /api/posts/1
Нажимаем кнопку Send. Если сообщение существует, то мы получим его в правой панели:
Если попробуем получить сообщение, которого не существует, то сервер нам ответит ошибкой 404
:
Получение всех сообщений
Чтобы получить все сообщения, используем GET
-метод для конечной точки: /posts
Создание нового пользователя Writer
Используем POST
-метод для конечной точки /users/writer
и добавим пользователя, которого хотим зарегистрировать, в тело JSON следующим образом:
{ "name": "User One", "email":"user1@me.com", "password":"password1", "password_confirmation":"password1" }
Нажатие кнопки Send
должно вернуть токен (сервер в базе данных пометит этот токен ролью writer
):
Любой Writer может создавать, обновлять, читать и удалять сообщения.
Создание нового пользователя Subscriber
Используем POST
-метод для конечной точки /users/subscriber
и добавим пользователя, которого хотим зарегистрировать, в тело JSON следующим образом:
{ "name": "User Subscriber", "email":"usersubscriber@me.com", "password":"password2", "password_confirmation":"password2" }
Нажатие кнопки Send должно вернуть токен (сервер в базе данных пометит этот токен ролью subscriber
):
Subscriber может только читать сообщения.
Заключение
На этом наш урок заканчивается. Мы научились создавать API-аутентификацию на основе нескольких ролей с помощью Laravel Sanctum. Я надеюсь вам всё было понятно.
Автор: DevsrealmGuy
Перевод: Алексей Широков
Наш Телеграм-канал — следите за новостями о Laravel.