Интересные задачи верстки и клиентского программирования

Суть авторизации в laravel. Политики и гейты

добавил Шубин Александр 12 Август, 2019


В ларавель есть штатная и хорошо продуманная система проверки доступа, но на мой взгляд в официальной документации она описывается не очень понятно. В этой статье я попытаюсь коротко объяснить суть происходящего, и далее понимая общую концепцию вы сможете легко добрать нужные детали в официальной документации.

Авторизация и аутентификация

Первое что нужно понять — различие между авторизацией (authorization) и аутентификацией (authentication). Звучит довольно похоже, но на самом деле это разные вещи. И не только в laravel.

Аутентификация

Аутентификация (authentication) это процесс логина (захода) на сайт, не важно каким способом. В процессе аутентификации мы неизвестного анонима (гостя) идентифицируем на сайте, после аутентификации хиты на странице делает уже не просто какой то рандомный IP адрес, а прямо конкретный Василий Пирогов. В интерфейсе появляется кнопка «выход», всякие страницы смены пароля и т.д. Вот это вот превращение анонима в известного пользователя, это процесс аутентификации.

Так что говорить «форма авторизации» в терминах ларавель неправильно, на самом деле это «форма аутентификации».

Авторизация

Авторизация (authorization) в ларавел это процесс проверки какой то конкретной возможности, доступности какого либо действия для пользователя. Authorization дословно переводится с английского как «разрешение». Действие которое мы разрешаем может быть абсолютно любым, например может ли пользователь поливать цветы. Вы самостоятельно прописываете логику по которой будете разрешать или запрещать действие пользователю, а потом в тот момент когда пользователь уже тянется к лейке, заложенная логика срабатывает и пользователь либо берет лейку и начинает полив, либо натыкается на прозрачную стену и грустно смотрит за стекло.

Если подходить ближе к практическим примерам, то реальной «возможностью» может быть например «возможность создания статьи». Вам в контроллер прилетает POST запрос, в котором лежит содержимое статьи, и в ответ на этот запрос вам нужно добавить статью и послать «ок», либо ответить ошибкой «доступ запрещен». Сам ларавель не знает можно ли текущему пользователю создавать статьи, он за вас ничего не решит. Вам нужно разрешить или запретить создание для пользователя, или другими словами авторизовать действие. Вот именно тут и начинают работать политики и гейты.

Политики и гейты

Два довольно громких слова, но в их основе лежит достаточно простой механизм. В целом можно сказать, что это одно и тоже. Просто разные варианты объявления функции с логикой проверки доступа. Для начала я покажу в чем их схожесть.

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

/**
 * post_create только для примера! Правильнее называть гейты через -, например post-create. Это позволит избежать пересечения с названиями функций в политиках
 */
Gate::define('post_create', function ($user) {
	return $user->isAdmin;
});

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

class ArticlePolicy
{
	public function post_create(User $user)
	{
		return $user->isAdmin;
	}
}

За кулисами после регистрации политики в AuthServiceProvider на самом деле произошла та же самая регистрация в гейте, только в другом свойстве.

Что такое «регистрация в гейте» которая происходит и там, и там? Речь идет про объект класса \Illuminate\Auth\Access\Gate. В этом классе есть свойства $abilities и $policies с типом array. Именно в этих свойствах добавляются новые элементы массива при регистрации гейтов и политик.

То есть другими словами мы и там и там описываем функцию которая вернет true / false, разрешить пользователю действие или запретить и уведомляем о наличии такой функции \Illuminate\Auth\Access\Gate.

Далее, после того как ларавел знает о наличии нашей функции проверки доступа, мы в нужном месте делаем проверку авторизации, например в контроллере авторизацию можно проверить так:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ArticleCreateRequest;
use App\Http\Resources\ArticleResource;
use App\Models\Article;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;

class ArticleController extends Controller
{
    /**
     * Store a newly created resource in storage.
     *
     * @param ArticleCreateRequest $request
     * @return ArticleResource
     * @throws AuthorizationException
     */
    public function store(ArticleCreateRequest $request)
    {
	// первый вариант проверки
        $this->authorize('post_create', Article::class);

	// второй вариант проверки
	/**
         * @var $user User
         */
        $user = Auth::user();
	if ($user->cant('post_create', Article::class)) {
            throw new AuthorizationException('This action is unauthorized.');
        }

	// третий вариант проверки
	if (Gate::denies('post_create', Article::class)) {
            throw new AuthorizationException('This action is unauthorized.');
        }

	// если не было исключения, то сохраняем статью
        $data = $request->validated();
        $article = new Article();
        $article->fill($data);
        $article->save();
        return new ArticleResource($article);
    }
}

В коде выше три варианта проверки. Все они вызывают нашу объявленную ранее функцию. Первый вариант проверки кидает exception самостоятельно, второй и третий возвращают true/false и по итогу нам нужно что-то с этим true/false сделать, в данном случае для демонстрации я тоже выбросил исключение.

И в самом конце примера, если наша функция авторизовала пользователя (разрешила выполнение и не выбросила исключений), то выполняем собственно само действие.

Итого здесь всего три шага:

  1. Пишем функцию с логикой проверки, она должна возвращать true/false и уведомляем фреймворк о наличии такой функции.
  2. Вызываем нашу функцию любым удобным способом, например через $this->authorize(…) или $user->can(…). В документации довольно много вариантов ее вызова.
  3. Выполняем само действие если пользователь прошел проверку

Я думаю суть происходящего понятна. Никакой магии тут нет.

В чем различия?

Но зачем тогда вообще вводить две разные сущности? По большей части это вопрос удобства. Главное что нужно понимать, в каких случаях вызывается гейт, а в каких функция из класса политики. Зная этот механизм вы в каждом конкретном случае выберите для себя наиболее подходящий вариант.

Если объявить сразу и гейт и политику с одинаковым именем, как например мы сделали выше в случае с post_create, то какую именно функцию выполнит laravel?

Всё достаточно просто, если вторым аргументом мы передаем какой-либо класс (или инстанс класса), и для этого класса зарегистрирована политика с нужной функцией, то вызовется именно функция из класса политики. Если же мы ничего не будем передавать вторым аргументом, то вызовется функция объявленная гейтом.

// будет вызвана функция из класса политики (если функция post_create существует)
$this->authorize('post_create', Article::class);

// будет вызвана функция из гейта
$this->authorize('post_create');

Отсюда следует вывод, что политики удобнее в случае когда нужно сгруппировать какие либо проверки авторизации. Не обязательно это кстати говоря должна быть группировка по модели. Если у нас есть группа каких-то схожих возможностей (доступных пользователю действий), то проще выделить их в отдельный класс и сложить в одном месте.

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

И гейты и политики вы определяете самостоятельно. Названия view, create, update и т.д. которые встречаются в примерах ничем не отличаются от проверки «water-flowers» (поливка цветов). Название у функции авторизации может быть абсолютно любое.

Я писал об этом в комментарии выше, но повторюсь здесь еще раз, правильнее вообще никогда не пересекать названия возможных проверок в гейтах и политиках, если вы пишете гейт то назовите его post-create через дефис, названия функций в php не могут содержать символ дефиса, поэтому просто глядя на проверку авторизации вы будете знать куда смотреть, если есть дефис, то это гарантированно гейт.

RBAC / ABAC

Немного теории и примеров. Вам скорее всего известна аббревиатура RBAC или по другому Role Bases Access Control. Это система разграничения прав доступа основанная на ролях. В ларавель достаточно просто реализовать RBAC, на вход в нашу функцию из гейта или политики приходит текущий пользователь (+инстанс модели по необходимости). Достаточно каким то образом хранить роль пользователя в БД и далее в зависимости от роли возвращать из функции true/false. Таким образом после смены роли пользователя в БД, его доступы ко всем возможностям приложения так же поменяются.

Реализация с ролями не сложная, но возможно будет правильнее проверять не роль пользователя, а доступ пользователя на совершение конкретного действия. То есть использовать ABAC модель доступа или по другому Attribute Based Access Control. Разграничение доступа на основе атрибутов. Модель ABAC так же отлично ложится на штатную систему авторизации ларавел.

Вот ссылка на статью с пояснениями по RBAC и ABAC. На практике у вас скорее всего будет какой то микс из этих моделей доступа. Пользователю при старте выдается какая либо роль или несколько ролей (к которым привязаны возможности), и далее доступ к конкретным действиям проверяется уже по отдельным возможностям роли (не просто по принадлежности пользователя к роли), плюс уточняется динамическими сравнениями и вычислениями.

Пакеты composer для авторизации

Система авторизации в коде подходит только для каких то простых случаев. Для более сложных потребуется хранение информации по ролям, атрибутам, возможностям и т.д. в базе данных. Для laravel есть несколько готовых composer пакетов, которые реализуют этот функционал.

Нужно понимать что эти пакеты просто дают возможность хранения доступов в БД, все запросы с учетом нужных сохраненных прав вам придется строить самостоятельно. Пакеты регистрируют свою функцию, которую вызовет ларавел при проверке авторизации, в тот момент когда вы проверяете $this->authorize(…) в контроллере, на самом деле выполняется какая то функция из конкретного пакета. То есть доступ проверяется не по свойствам $abilities / $policies объекта с классом \Illuminate\Auth\Access\Gate, а в функции.

Для удобства можно комбинировать возможности политик и функционал пакетов — проверять $user->can(…) уже внутри функции политики.

JosephSilber Bouncer

Ссылка на гитхаб https://github.com/JosephSilber/bouncer. Стоит обратить внимание на возможность этого пакета хранить доступ отдельного пользователя к отдельной сущности. Например можно дать доступ конкретному пользователю на редактирование конкретной статьи.

Еще из особенностей про которые стоит сказать — возможность хранения ролей и доступов независимо для нескольких срезов пользователей одновременно. Например если у вас есть какой то общий код, общее приложение с одной БД в которой работают несколько разных компаний клиентов (или просто команд внутри одной организации), то вы можете для каждой компании клиента хранить собственные независимые роли и наборы доступов, bouncer предоставляет штатный функционал для создания выборок по каждой компании клиенту независимо. В каждой таблице есть отдельная колонка scope и middleware, который накладывает scope на все запросы к таблицам с доступами.

Spatie Laravel Permission

Ссылка на github https://github.com/spatie/laravel-permission. Пакет на мой взгляд чуть попроще, но если вам нужна простая и понятная система хранения доступов, то можете рассмотреть и этот вариант. Просто поставил и сразу используешь, вникать нужно поменьше.

Итого

Надеюсь мне удалось показать именно суть авторизации. Теперь когда у вас есть центральный стержень понимания в документации вы можете посмотреть конкретные детали. Я не затронул еще много штатных возможностей, например проверку авторизации в middleware. Хотя бы пролистать документацию в любом случае стоит.

Можете так же посмотреть дополнительные статьи по теме:

Надеюсь статья оказалась вам полезной, если это действительно так, то можете сказать спасибо в комментариях 🙂


добавил Шубин Александр 12 Август, 2019
Рубрика: laravel


6 комментариев

  • Евгений:

    Спасибо, всё доступно объяснено. Пришлось 3 раза перечитать оф-документацию, чтобы вникнуть. Со шлюзами было сначала непонятно — думал что это какой-то своеобразный маршрут, но теперь всё более-менее понятно. Ваша статья кое-что прояснила, спасибо!

  • Евгений:

    Ещё вопрос: получается когда в контроллере проводим проверку $this->authorize(‘post_create’, Article::class);, то наш $user берётся автоматически из сессии (хранилища)? Т.е. не нужно сначала делать запрос к бд и потом уже передавать экземпляр $user в authorize()?

  • АнтиПутин:

    Ема конечно написали про Аутентификацию и Авторизацию сложно.

    Проще :

    Идентификация — способ предоставления данных,для аутентификации.

    Аутентификация — способ проверки предоставленных данных,на основе идентификации.

    Авторизация — последний этап,при котором два предыдущих (идентификация и аутентификация) являются успешным. Авторизация наделяет пользователя привилегиями,согласно логике(создавать что-либо, редактировать,удалять, и так далее)

    • Спасибо за комментарий. Если заранее знать все три термина, то определения понятны. Я же более простым языком попытался суть происходящего объяснить. Пусть и длиннее получилось, но на мой взгляд наоборот проще

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

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