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

от автора

Введение

Здравствуйте, дорогие Хабровчане.

В ходе своей работы над 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 в классе PostController не существует в стандартных методах класов типа 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 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *