Pull to refresh

Продвинутая система авторизации действий с ресурсами в Laravel. Часть 1. Модель, Контроллер

Reading time6 min
Views8.5K

Введение


Здравствуйте, дорогие Хабровчане.
В ходе своей работы над api-платформой я провел много времени в поисках верного пути авторизации действий пользователя. Задача была поставлена такая — создать довольно разветвленную систему контроля доступа и действий.
При этом большинство из них на обычный CRUD, но необходимо будет авторизовать и другие действия контролера.
А значит необходимо создать простую и в то же время эффективную и гибкую систему. Шишек было набито немало, потому в этих статьях я решил продемонстрировать несколько упрощенную версию того что у меня получилось.


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



Часть 1. Модель, Контроллеры


Итак, задача: Имеется много моделей, количество которых может увеличиваться, и уменьшаться по ходу развития проекта.
Действия над каждой из них должны авторизоваться по ролях.
Присутствуют и простые действия, такие как CRUD, и дополнительные (к примеру import, export).
Необходимо максимально упростить работу разработчику (себе любимому) для внесения дополнительных моделей и методов к ним.
В статье приводится пример для Api приложения, но решение подходит не только для такового.


Модель


Для демонстрации создадим модель Post. Я предпочитаю отойти от не целесообразной (в нашем случае) практики хранения моделей в корне папки app.
Потому создаю Папку app/Models а в ней модель Posts. (Предполагается что моделей будет достаточно много, иначе зачем тогда весь сыр-бор).
К модели создаем таблицу. Для этого в консоли выполняем команды:


    php artisan make:model Models/Post --api --migration
    php artisan migrate

В результате мы получим стандартные: модель, миграцию и контроллер. На модели и миграции останавливаться не стоит, так как в рамках данной статьи их я изменять не буду.


Контроллер


Мы уже имеем сгенерированный контроллер PostController с базовыми CRUD операциями
и стандартный родительский класс Controller.
Еще нам понадобится создать абстрактный класс ModelController который расширит клас Controller и от которого, в свою очередь, унаследуем наш PostController.
В сам же PostController для демонстрации добавим еще два метода — import() и export(Post $post).
Я не стану останавливаться на действиях над ресурсами, так как это не есть предметом статьи.


А сейчас немного теории


В Laravel существует стандартный трейт Illuminate\Foundation\Auth\Access\AuthorizesRequests.
В нашем случае он расширяет стандартный клас контроллера App\Http\Controllers\Controller от которого мы унаследуем свой ModelController. Сам же Controller я сознательно изменять не буду, так как попытка перезаписывать методы трейта в стандартном контроллере приведет к ошибке во время исполнения.
Этот трейт в своем арсенале имеет метод authorizeResource($model).
Он может принимать аргументом строковое имя класса, и генерировать соответствующие посредники (Middleware) для доступа к методам контроллера.
Забегая немного наперед продемонстрирую вам пример генерируемых посредников:


    can:viewAny,App\Models\Post
    can:view,post
    can:update,post

Как вы, возможно, заметили — здесь есть две особенности:


  1. Методов viewAny и view не существует в стандартных методах классов типа apiResource
  2. В одном случае используется имя класса, в других — экземпляра модели.

Об этом по порядку:


Первое. Для определения соответствия метода контроллера и значения в аргументе посредника существует стандартный метод resourceAbilityMap() Он и задает это соответствие.


<?php
protected function resourceAbilityMap()
{
    return [
        'index' => 'viewAny',
        'show' => 'view',
        'create' => 'create',
        'store' => 'create',
        'edit' => 'update',
        'update' => 'update',
        'destroy' => 'delete',
    ];
}

Второе. В Laravel существует два типа авторизации действий. Действие с экземпляром модели, и действие без экземпляра модели. К примеру метод просмотра списка index() не имеет экземпляра модели, а метод show(Post $post) имеет. Для определения соответствия фремворк использует метод resourceMethodsWithoutModels()


<?php
protected function resourceMethodsWithoutModels()
{
    return ['index', 'create', 'store'];
}

Перейдем к практике


Базовый контроллер (ModelController)

В директории app/Http/Controllers Создаем абстрактный класс ModelController. В нем определяем следующее:


  • Свойство $guardedMethods содержит в себе список пар значений «метод контроллера» => «метод политики».
    О методах политики(Policy) подробней будет написано во второй части статьи, и все же стоит уточнить, что на самом деле это имя Шлюза(Gate). Но в конечном итоге мы придем именно к тому что здесь будет записываться метод политики.
  • Свойство $methodsWithoutModels содержит в себе список методов которые не имеют экземпляра модели.
  • Абстрактный метод getModelClass() обязует программиста определить в дочернем контроллере соответствующий метод.
    Это даст нам возможность использовать его в качестве аргумента для метода авторизации ресурсов.
  • Конструктор посредством метода authorizeResource($this->getModelClass()) запускает генерацию посредников для защиты методов. (Точнее для защиты путей(Route), но это уже нюансы)
  • Методы resourceAbilityMap() и resourceMethodsWithoutModels() функциональность которых описана выше, с той лишь разницей, что наши методы будут дополнять стандартные значения.

<?php

namespace App\Http\Controllers;

abstract class ModelController extends Controller
{
    /** @var array 'method' => 'policy'*/
    protected $guardedMethods = [];

    protected $methodsWithoutModels = [];

    protected abstract function getModelClass(): string;

    public function __construct()
    {
        $this->authorizeResource($this->getModelClass());
    }

    protected function resourceAbilityMap()
    {
        $base = parent::resourceAbilityMap();

        return array_merge($base, $this->guardedMethods);
    }

    protected function resourceMethodsWithoutModels()
    {
        $base = parent::resourceMethodsWithoutModels();

        return array_merge($base, $this->methodsWithoutModels);
    }
}

Контроллер модели (PostController)

Контроллер модели будет содержать в себе следущее:


  • Определение метода getModelClass() который должен отдавать строковое имя класса модели.
  • Свойство $methodsWithoutModels в котором запишем пары значений «метод контроллера» => «метод политики» для дополнительных методов. Стандартные методы уже учтены. Здесь стоит отметить что данные пары значений можно называть по разному, но я для удобства предлагаю называть по принципу ключ=значение. Данную реализацию можно еще улучшить путем автоматической генерации данных пар, но это уже вам виднее, в зависимости от задачи.
  • Свойство $methodsWithoutModels в котором запишем имена дополнительных методов, которые не оперируют экземпляром модели. В нашем случае import
  • Чтобы было более понятно — я подписал какие именно методы имеют экземпляр модели, а какие нет. Повторюсь — действия внутри методов контроллера не входят в тематику этой статьи.

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends ModelController
{
    /** @var array 'method' => 'policy'*/
    protected $guardedMethods = [
        'export' => 'export',
        'import' => 'import',
    ];

    protected $methodsWithoutModels = ['import'];

    protected function getModelClass(): string
    {
        return Post::class;
    }

    public function index()
    { /** Не имеет экземпляра модели */ }

    public function store(Request $request)
    { /** Имеет экземпляр модели */ }

    public function show(Post $post)
    { /** Имеет экземпляр модели */ }

    public function update(Request $request, Post $post)
    { /** Имеет экземпляр модели */ }

    public function destroy(Post $post)
    { /** Имеет экземпляр модели */ }

    public function import()
    { /** Не имеет экземпляра модели */ }

    public function export(Post $post)
    { /** Имеет экземпляр модели */ }
}

Пути (Routes)


Настало время открыть доступ к нашим методам.
Переходим к файлу 'routes/api.php' (или 'routes/web.php', в зависимости от задачи) и прописываем там доступ к методам.
И несмотря на то, что эта задача достаточно тривиальна, все же стоит отметить два нюанса.
Первый — предпочтительно (а в нашем случае необходимо) располагать ваши дополнительные пути выше стандартных, генерируемых методом Route::apiResource('posts', 'PostController'). Это обусловлено принципом маршрутизации Laravel. Более частные случаи нужно располагать выше общих.
Второй — вовсе не обязательно прописывать посредника 'auth:api'. Система авторизации вполне может работать и на принципах, не зависящих от аутентификации пользователя.


<?php

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::group(['middleware' => ['auth:api']], static function () {
    Route::post('posts/import', 'PostController@import')->name('posts.import');
    Route::get('posts/{post}/export', 'PostController@export')->name('posts.export');
    Route::apiResource('posts', 'PostController');
});

А сейчас проверим все ли правильно сделали. В консоли выполняем команду:


php artisan route:list

Если все сделано верно — видим результат (я сократил таблицу до необходимомого минимума)


+-----------------------+---------------------------+-------------------------------+
|URI                    |Action                     |Middleware                     |
+-----------------------+---------------------------+-------------------------------+
|api/posts              |...\PostController@index   |...,can:viewAny,App\Models\Post|
|api/posts              |...\PostController@store   |...,can:create,App\Models\Post |
|api/posts/import       |...\PostController@import  |...,can:import,App\Models\Post |
|api/posts/{post}       |...\PostController@show    |...,can:view,post              |
|api/posts/{post}       |...\PostController@update  |...,can:update,post            |
|api/posts/{post}       |...\PostController@destroy |...,can:delete,post            |
|api/posts/{post}/export|...\PostController@export  |...,can:export,post            |
+-----------------------+---------------------------+-------------------------------+



Следующий этап — настройка связки Шлюз(Gate)<->Политика(Policy). Но об этом уже во второй части.

Tags:
Hubs:
+3
Comments4

Articles