Из этой статьи вы узнаете, как реализовать систему аутентификации на основе ролей пользователей в своем приложении с помощью 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.
