Версия фреймворка: 6.x 5.4 4.2

Авторизация

Введение

В дополнение к изначально предоставленным службам аутентификации, Laravel также предоставляет способ авторизовать действия пользователя в отношении некого ресурса - возможность разрешить или запретить некоторое действие. Как и в случае с аутентификацией, подход Laravel к авторизации прост, и есть два основных способа авторизации действий: шлюзы (gates) и политики (policies).

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

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

Шлюзы

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

Шлюзы (гейты, gates) - это анонимные функции, которые определяют, имеет ли пользователь право выполнить данное действие; они обычно определяются в классе App\Providers\AuthServiceProvider с помощью фасада Gate. Шлюзы всегда получают экземпляр пользователя в качестве первого аргумента. Также они могут принимать дополнительные аргументы, например, соответствующую модель 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')) {
    // The current user can edit settings
}

if (Gate::allows('update-post', $post)) {
    // The current user can update the post...
}

if (Gate::denies('update-post', $post)) {
    // The current user can't update the post...
}

Если вы хотите определить, авторизован ли конкретный пользователь для выполнения действия, можно использовать метод forUser фасада Gate:

if (Gate::forUser($user)->allows('update-post', $post)) {
    // The user can update the post...
}

if (Gate::forUser($user)->denies('update-post', $post)) {
    // The user can't update the post...
}

Можно использовать несколько проверок:

if (Gate::any(['update-post', 'delete-post'], $post)) {
    // The user can update or delete the post
}

if (Gate::none(['update-post', 'delete-post'], $post)) {
    // The user cannot update or delete the post
}

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

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

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

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

Дополнительный контекст

Методы авторизации (allows, denies, check, any, none, authorize, 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])) {
    // The user can create the post...
}

Развёрнутые ответы от шлюзов

Ранее мы получали от шлюзов ответ в виде двоичных значений - true или false. Но иногда нам требуется получить более детальный ответ, с сообщением что конкретно пошло не так. Для этого можно вернуть объект 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('You must be a super administrator.');
});

Gate::allows будет по прежнему возвращать логическое значение, а чтобы получить подробный ответ от шлюза - можно использовать Gate::inspect:

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

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

В этом варианте Gate::authorize, бросая исключение AuthorizationException будет добавлять заданный текст ошибки авторизации в HTTP-ответ:

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

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

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

Если вам нужно дать некоторому пользователю (непример, суперадмину) права проходить любые шлюзы, воспользуйтесь методом before, который автоматически запускается перед любой проверкой авторизации шлюзами:

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

Если before возвращает что-то отличное от null, это значение рассматривается как результат проверки.

Также существует метод after, он вызывается после прохождения всех проверок:

Gate::after(function ($user, $ability, $result, $arguments) {
    if ($user->isBanned()) {
        return false; // если пользователь забанен, любые разрешения на действия являются недействительными
    }
});

Как и в случае before , отличное от null возвращаемое значение рассматривается как результат проверки авторизации.

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

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

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

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

php artisan make:policy PostPolicy

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

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

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

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

<?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

Развёрнутые ответы политик

Обычно результат работы политик - ответ в виде значений true или false. Но иногда нам требуется получить более детальный ответ, с сообщением, что конкретно не так. Для этого можно вернуть объект 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('You do not own this post.');
}

Gate::allows будет по прежнему возвращать логическое значение, а чтобы получить подробный ответ от шлюза - можно использовать Gate::inspect:

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

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

В этом варианте Gate::authorize, бросая исключение 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 для незалогиненных пользователей, запрещая любые действия. Вы можете обойти это поведение и разрешить проверку для незалогиненных пользователей, если укажете в аргументах, что вместо экзепляра класса User в метод проверки может прийти 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, которая поставляется с Laravel, включает в себя два полезных метода для авторизации действий: can и cant. Метод can принимает действие, которое вы хотите разрешить, и соответствующую модель. Например, давайте определим имеет ли пользователь право обновлять данную модель Post:

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

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

Действия которые не требуют моделей

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

use App\Post;

if ($user->can('create', Post::class)) {
    // Executes the "create" method on the relevant policy...
}

Через посредников

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

use App\Post;

Route::put('/post/{post}', function (Post $post) {
    // The current user may update the post...
})->middleware('can:update,post');

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

Действия которые не требуют моделей

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

Route::post('/post', function () {
    // The current user may create posts...
})->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 Illuminate\Http\Request;
use App\Post;
use App\Http\Controllers\Controller;

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);

        // The current user can update the blog 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);

    // The current user can create blog posts...
}

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

Если вы используете ресурсные контроллеры, вы можете упростить написание авторизации, использовав authorizeResource в конструкторе такого контроллера.

authorizeResource принимает название класса модели в качестве первого аргумента и имя роута / параметр запроса, который содержит ID экземпляра этой модели в качестве второго аргумента:

<?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');
    }
}

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

Метод контроллера Метод политики
index viewAny
show view
create create
store create
edit update
update update
destroy delete
Вы можете создать класс политики для заданной модели следующей командой: 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)
    <!-- The Current User Can Create Posts -->
@endcan

@cannot('create', App\Post::class)
    <!-- The Current User Can't Create Posts -->
@endcannot

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

Во время вызова проверок авторизации $this->authorize второй аргумент может также быть массивом. В этом случае первый элемент массива будет считаться моделью, чья политика будет использоваться, а остальные аргументы могут быть использованы для дополнительных данных, требующихся для проверки. Например, для PostPolicy может понадобиться не только сам пост, но и номер категории:

/**
 * Определить, может ли данный пользователь редактировать данный пост
 *
 * @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;
}

Вызов проверки авторизации будет выглядеть так:

/**
 * Редактирование поста
 *
 * @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')]);

    // The current user can update the blog post...
}