Авторизация

Вступление

В дополнение к аутентификации, Laravel также предоставляет простой способ авторизации действий пользователя в отношении ресурсов. Как и аутентификация, подход Ларавела к авторизации прост, и существует два основных способа авторизации действий: шлюзы и политики.

Подумайте о шлюзах и политиках, как о маршрутах и контроллерах. Шлюзы обеспечивают простой, основанный на закрытии подход к авторизации, в то время как политики, как контроллеры, группируют свою логику вокруг определенной модели или ресурса. Сначала мы рассмотрим шлюзы, а затем политики.

Вам не нужно выбирать между использованием только шлюзов или только политик. Большинство приложений, скорее всего, будут содержать смесь шлюзов и политик, и это совершенно нормально! Шлюзы наиболее применимы к действиям, которые не связаны с какой-либо моделью или ресурсом, например, просмотр панели администратора. Напротив, политики следует использовать, когда вы хотите авторизовать действие для определенной модели или ресурса.

Шлюзы (Gates)

Написание шлюзов

Шлюзы — это замыкания (closure), которые определяют, уполномочен ли пользователь выполнять заданное действие, и обычно определяются в классе App\Providers\AuthServiceProvider с помощью фасада Gates. Глюзы всегда получают экземпляр пользователя в качестве первого аргумента и может опционально получать дополнительные аргументы, такие как соответствующая модель Eloquent:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Gate::define('edit-settings', function ($user) {
        return $user->isAdmin;
    });

    Gate::define('update-post', function ($user, $post) {
        return $user->id === $post->user_id;
    });
}

Шлюзы, как и контроллеры, могут быть определены с помощью строки обратного вызова в стиле Class@method:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Gate::define('update-post', 'App\Policies\PostPolicy@update');
}

Авторизация действий

Чтобы авторизовать действие с использованием шлюзов, вы должны использовать методы allows или denies. Обратите внимание, что вы не обязаны передавать аутентифицированного пользователя этим методам. Laravel автоматически позаботится о прохождении пользователя в замыкание шлюза:

if (Gate::allows('edit-settings')) {
    // Текущий пользователь может редактировать настройки
}

if (Gate::allows('update-post', $post)) {
    // Текущий пользователь может обновить пост...
}

if (Gate::denies('update-post', $post)) {
    // Текущий пользователь не может обновить пост...
}

Для определения того, уполномочен ли определенный пользователь выполнять какое-либо действие, вы можете использовать метод forUser фасада Gate:

if (Gate::forUser($user)->allows('update-post', $post)) {
    // Пользователь может обновить пост...
}

if (Gate::forUser($user)->denies('update-post', $post)) {
    // Пользователь не может обновить пост...
}

Вы можете санкционировать несколько действий одновременно с помощью методов any или none:

if (Gate::any(['update-post', 'delete-post'], $post)) {
    // Пользователь может обновить или удалить пост
}

if (Gate::none(['update-post', 'delete-post'], $post)) {
    // Пользователь не может обновить или удалить пост
}

Авторизация или выброс исключения

Если вы хотите попытаться авторизовать действие и автоматически бросить Illuminate\Auth\Access\AuthorizationException, если пользователю не разрешено выполнять данное действие, вы можете воспользоваться методом Gate::authorize. Экземпляр AuthorizationException автоматически конвертируются в HTTP ответ 403:

Gate::authorize('update-post', $post);

// Действие разрешено...

Предоставление дополнительного контекста

Методы авторизации шлюза (allows, denies, check, any, none, autorize, can, cannot) и Blade-директивы авторизации (@can, @cannot, @canany) могут получить массив в качестве второго аргумента. Эти элементы массива передаются в качестве параметров к шлюзу и могут быть использованы для дополнительного контекста при принятии решения о авторизации:

Gate::define('create-post', function ($user, $category, $extraFlag) {
    return $category->group > 3 && $extraFlag === true;
});

if (Gate::check('create-post', [$category, $extraFlag])) {
    // Пользователь может создать пост...
}

Ответ шлюза

До сих пор мы исследовали только те шлюзы, которые возвращают простые булевы значения. Тем не менее, иногда вы можете захотеть вернуть более подробный ответ, включая сообщение об ошибке. Для этого вы можете вернуть из шлюза Illuminate\Auth\Access\Response:

use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;

Gate::define('edit-settings', function ($user) {
    return $user->isAdmin
                ? Response::allow()
                : Response::deny('Вы должны быть супер-администратором.');
});

При возврате ответа на авторизацию от шлюза, метод Gate::allow все равно вернет простое логическое значение. Однако, вы можете использовать метод Gate::inspect для получения полного ответа на авторизацию, возвращаемого шлюзами:

$response = Gate::inspect('edit-settings', $post);

if ($response->allowed()) {
    // Действие разрешено...
} else {
    echo $response->message();
}

Конечно, при использовании метода Gate::authorized выбрасывающего AuthorizationException, если действие не авторизовано, сообщение об ошибке, предоставленное ответом авторизации, будет передано в ответ HTTP:

Gate::authorize('edit-settings', $post);

// Действие разрешено...

Перехват проверок шлюза

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

Gate::before(function ($user, $ability) {
    if ($user->isSuperAdmin()) {
        return true;
    }
});

Если функция обратного вызова в before возвращает ненулевой результат, то этот результат будет считаться результатом проверки.

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

Gate::after(function ($user, $ability, $result, $arguments) {
    if ($user->isSuperAdmin()) {
        return true;
    }
});

По аналогии с проверкой before, если в функции обратного вызова after возвращается ненулевой результат, то этот результат будет считаться результатом проверки.

Создание политик

Генерация политик

Политики — это классы, которые организуют логику авторизации вокруг определенной модели или ресурса. Например, если ваше приложение является блогом, у вас может быть модель Post и соответствующая политика PostPolicy для авторизации действий пользователя, вроде создания или обновления сообщений.

Вы можете сгенерировать политику, используя artisan-команду make:policy. Сгенерированная политика будет помещена в каталог app/Policies. Если этот каталог не существует в вашем приложении, Laravel создаст его:

php artisan make:policy PostPolicy

Команда make:policy сгенерирует пустой класс политики. Если Вы хотите сгенерировать класс с базовыми методами "CRUD", то можете указать ключ --model при выполнении команды:

php artisan make:policy PostPolicy --model=Post

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

Регистрация политик

Существующая политика должна быть зарегистрирована. AuthServiceProvider в комплекте со свежими приложениями Laravel содержит свойство polisies, которое сопоставляет ваши Eloquent-модели с их политиками. Регистрация политики инструктирует Laravel, какую политику использовать при авторизации действий данной модели:

<?php

namespace App\Providers;

use App\Policies\PostPolicy;
use App\Post;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        //
    }
}

Авто-обнаружение политик

Вместо ручной регистрации политик модели, Laravel может автоматически обнаруживать политики до тех пор, пока модель и политика следуют стандартным конвенциям об именовании. Если говорить точнее, политики должны находиться в каталоге Policies внутри каталога, содержащего модели. Так, например, модели могут быть помещены в каталог app, в то время как политики могут быть помещены в каталог app/Policies. Кроме того, имя политики должно совпадать с именем модели и иметь суффикс Policy. Таким образом, модель User будет соответствовать классу политики UserPolicy.

Если вы хотите предоставить свою собственную логику обнаружения политики, вы можете зарегистрировать пользовательский функцию обратного вызова, используя метод Gate::guessPolicyNamesUsing. Обычно этот метод должен вызываться из метода boot класса AuthServiceProvider:

use Illuminate\Support\Facades\Gate;

Gate::guessPolicyNamesUsing(function ($modelClass) {
    // вернуть имя класса политики...
});

Любые политики, которые явно отображаются в вашем AuthServiceProvider, будут иметь приоритет над любыми потенциальными автообнаруженными политиками.

Написание политик

Методы потитик

После регистрации политики вы можете добавлять методы для каждого санкционированного ею действия. Например, давайте определим метод update в нашей PostPolicy, который определяет, может ли данный User обновлять данный экземпляр Post.

Метод update будет получать в качестве аргументов экземпляры User и Post, и должен возвращать true или false, указывая, уполномочен ли пользователь обновлять данный Post. Итак, для данного примера, давайте проверим, совпадает ли id пользователя с user_id поста:

<?php

namespace App\Policies;

use App\Post;
use App\User;

class PostPolicy
{
    /**
     * Determine if the given post can be updated by the user.
     *
     * @param  \App\User  $user
     * @param  \App\Post  $post
     * @return bool
     */
    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}

Вы можете продолжать определять дополнительные методы в политике, необходимые для различных действий, которые она разрешает. Например, Вы можете определить методы view или delete для авторизации различных действий Post, но помните, что Вы можете дать своим методам политики любое имя, которое Вам понравится.

Если Вы использовали опцию --model при генерации своей политики через консоль Artisan, то она уже будет содержать методы для действий viewAny, view, create, update, delete, restore и forceDelete.

Ответ политики

До сих пор мы рассматривали только методы политики, которые возвращают простые булевые значения. Однако, иногда вы можете захотеть вернуть более подробный ответ, включая сообщение об ошибке. Для этого вы можете вернуть Illuminate\Auth\Access\Response из метода вашей политики:

use Illuminate\Auth\Access\Response;

/**
 * Determine if the given post can be updated by the user.
 *
 * @param  \App\User  $user
 * @param  \App\Post  $post
 * @return \Illuminate\Auth\Access\Response
 */
public function update(User $user, Post $post)
{
    return $user->id === $post->user_id
                ? Response::allow()
                : Response::deny('Ты не владеешь этим постом.');
}

При возврате ответа на авторизацию из вашей политики, метод Gate::allow все равно будет возвращать простое логическое значение. Однако, вы можете использовать метод Gate::inspect для получения полного ответа на авторизацию, возвращаемого шлюзом:

$response = Gate::inspect('update', $post);

if ($response->allowed()) {
    // Действие разрешено...
} else {
    echo $response->message();
}

Конечно, при использовании метода Gate::authorized бросающего исключение AuthorizationException, если действие не авторизовано, сообщение об ошибке, предоставленное ответом авторизации, будет передано в ответ HTTP:

Gate::authorize('update', $post);

// Действие разрешено...

Методы без экземпляра модели

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

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

/**
 * Determine if the given user can create posts.
 *
 * @param  \App\User  $user
 * @return bool
 */
public function create(User $user)
{
    //
}

Гостевые пользователи

По умолчанию, все шлюзы и политики автоматически возвращают false, если входящий HTTP-запрос не был инициирован аутентифицированным пользователем. Однако, вы можете разрешить этим проверкам авторизации пройти к вашим шлюзам и политикам, объявив "необязательный" параметр или предоставив null в качестве значения по умолчанию для определения аргумента пользователя:

<?php

namespace App\Policies;

use App\Post;
use App\User;

class PostPolicy
{
    /**
     * Determine if the given post can be updated by the user.
     *
     * @param  \App\User  $user
     * @param  \App\Post  $post
     * @return bool
     */
    public function update(?User $user, Post $post)
    {
        return optional($user)->id === $post->user_id;
    }
}

Фильтры политики

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

public function before($user, $ability)
{
    if ($user->isSuperAdmin()) {
        return true;
    }
}

Если вы хотите отказать пользователю во всех авторизациях, вы должны вернуть false из метода before. Если возвращается null, авторизация будет попадать под действие метода политики.

Метод before класса политики не будет вызван, если класс не содержит метода с именем, совпадающим с именем проверяемого действия.

Авторизация действий с использованием политик

С помощью модели пользователя (User)

Модель User, которая входит в состав вашего приложения Laravel, включает в себя два полезных метода для авторизации действий: can и cant. Метод can получает действие, которое вы хотите авторизовать, и соответствующую модель. Например, давайте определим, авторизован ли пользователь на обновление заданной модели Post:

if ($user->can('update', $post)) {
    //
}

Если для данной модели зарегистрирована политика, метод can автоматически вызовет соответствующую политику и вернет булевый результат. Если политика для данной модели не зарегистрирована, метод can попытается вызвать шлюз на основе Closure, соответствующий названию данного действия.

Действия, которые не требуют экземпляра модели

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

use App\Post;

if ($user->can('create', Post::class)) {
    // Выполнится метод "create" соответствующей политики....
}

С помощью посредника

Laravel включает в себя посредника, который может авторизовать действия еще до того, как входящий запрос достигнет ваших маршрутов или контроллеров. По умолчанию посреднику Illuminate\Auth\Middleware\Authorize присвоен ключ can в вашем классе App\Http\Kernel. Рассмотрим пример использования посредника can для авторизации того, что пользователь может обновлять запись в блоге:

use App\Post;

Route::put('/post/{post}', function (Post $post) {
    // Текущий пользователь может обновить сообщение...
})->middleware('can:update,post');

В этом примере мы передаем два аргумента посреднику can. Первый — это имя действия, которое мы хотим авторизовать, а второй — параметр маршрута, который мы хотим передать методу политики. В данном случае, так как мы используем неявную привязку модели, в метод политики будет передана модель Post. Если пользователь не авторизован на выполнение данного действия, посредником будет сгенерирован HTTP-ответ с кодом состояния 403.

Действия, которые не требуют экземпляра модели

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

Route::post('/post', function () {
    // Текущий пользователь может создавать сообщения...
})->middleware('can:create,App\Post');

С помощью помощников контроллера

В дополнение к методам, предусмотренным для модели User, Laravel предоставляет полезный метод authorize для любого из ваших контроллеров, который расширяет базовый класс App\Http\Controllers\Controller. Как и метод can, этот метод принимает имя действия, которое вы хотите авторизовать, и соответствующую модель. Если действие не авторизовано, метод authorize бросит исключение Illuminate\Auth\Access\AuthorizationException, которое обработчик исключений Laravel по умолчанию будет преобразовывать в HTTP-ответ с кодом состояния 403:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    /**
     * Update the given blog post.
     *
     * @param  Request  $request
     * @param  Post  $post
     * @return Response
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        // Текущий пользователь может обновить запись в блоге...
    }
}

Действия, которые не требуют экземпляра модели

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

/**
 * Create a new blog post.
 *
 * @param  Request  $request
 * @return Response
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function create(Request $request)
{
    $this->authorize('create', Post::class);

    // Текущий пользователь может создавать записи в блоге...
}

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

Если Вы используете контроллеры ресурсов, то в конструкторе контроллера можно использовать метод authorizeResource. Этот метод прикрепит к методам контроллера ресурсов соответствующие определения посредников can.

Метод authorizeResource принимает в качестве первого аргумента имя класса модели, а в качестве второго аргумента — имя параметра route / request, который будет содержать идентификатор модели. Необходимо убедиться, что ваш контроллер ресурса создан с флагом --model для того, чтобы иметь необходимые сигнатуры методов и подсказки типов:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Post::class, 'post');
    }
}

Следующие методы контроллеров будут привязаны к их соответствующим методам политики:

Вы можете использовать команду make:policy с опцией --model для быстрой генерации класса политики для данной модели: php artisan make:policy PostPolicy --model=Post.

В шаблонах Blade

При написании Blade-шаблонов вы можете захотеть отобразить часть страницы только в том случае, если пользователь имеет право на выполнение данного действия. Например, вы можете захотеть показать форму обновления для записи в блоге только в том случае, если пользователь может действительно обновить запись. В этой ситуации Вы можете использовать директивы @can и @cannot:

@can('update', $post)
    <!-- Текущий пользователь может обновить пост -->
@elsecan('create', App\Post::class)
    <!-- Текущий пользователь может создавать новые посты -->
@endcan

@cannot('update', $post)
    <!-- Текущий пользователь не может обновить пост -->
@elsecannot('create', App\Post::class)
    <!-- Текущий пользователь не может создать новый пост. -->
@endcannot

Эти директивы являются удобными сокращениями для написания @if и @unless выражений. Вышеуказанные утверждения @can и @cannot соответствуют следующим выражениям:

@if (Auth::user()->can('update', $post))
    <!-- Текущий пользователь может обновить сообщение -->
@endif

@unless (Auth::user()->can('update', $post))
    <!-- Текущий пользователь не может обновить сообщение -->
@endunless

Вы также можете определить, имеет ли пользователь какие-либо разрешения из заданного списка. Для этого используйте директиву @canany:

@canany(['update', 'view', 'delete'], $post)
    // Текущий пользователь может обновить, просмотреть или удалить сообщение.
@elsecanany(['create'], \App\Post::class)
    // Текущий пользователь может создать сообщение
@endcanany

Действия, которые не требуют экземпляра модели

Как и большинство других методов авторизации, вы можете передать имя класса в директивы @can и @cannot, если действие не требует экземпляра модели:

@can('create', App\Post::class)
    <!-- Текущий пользователь может создавать сообщения -->
@endcan

@cannot('create', App\Post::class)
    <!-- Текущий пользователь не может создавать сообщения -->
@endcannot

Предоставление дополнительного контекста

При авторизации действий с помощью политик вы можете передать массив в качестве второго аргумента различным функциям авторизации и помощникам. Первый элемент массива будет использоваться для определения того, какая политика должна быть вызвана, а остальные элементы массива передаются в качестве параметров методу политики и могут использоваться в дополнительном контексте при принятии решений о авторизации. Например, рассмотрим следующее определение метода PostPolicy, который содержит дополнительный параметр $category:

/**
 * Determine if the given post can be updated by the user.
 *
 * @param  \App\User  $user
 * @param  \App\Post  $post
 * @param  int  $category
 * @return bool
 */
public function update(User $user, Post $post, int $category)
{
    return $user->id === $post->user_id &&
           $category > 3;
}

При попытке определить, может ли аутентифицированный пользователь обновить данное сообщение, мы можем использовать этот метод политики:

/**
 * Update the given blog post.
 *
 * @param  Request  $request
 * @param  Post  $post
 * @return Response
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function update(Request $request, Post $post)
{
    $this->authorize('update', [$post, $request->input('category')]);

    // Текущий пользователь может обновить запись в блоге...
}

Last updated