Создание REST API c Ролями и Правами

role based api authentication

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