Введение
Здравствуйте, дорогие Хабровчане.
В ходе своей работы над 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
Как вы, возможно, заметили — здесь есть две особенности:
- Методов viewAny и view в классе PostController не существует в стандартных методах класов типа apiResource
- В одном случае используется имя класса, в других — экземпляра модели.
Об этом по порядку:
Первое. Для определения соответствия метода контроллера и значения в аргументе посредника существует стандартный метод 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 create() { /** Не имеет экземпляра модели */ } public function store(Request $request) { /** Имеет экземпляр модели */ } public function show(Post $post) { /** Имеет экземпляр модели */ } public function edit(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). Но об этом уже во второй части.
ссылка на оригинал статьи https://habr.com/ru/post/497220/
Добавить комментарий