Авторизация

Введение

Вместе аутентификацией пользователей в Laravel есть средства авторизации, т.е. средства управления доступом к различным ресурсам приложения.

Установка правил авторизации

Самый простой способ определить, есть ли права на некоторую операцию у данного пользователя - определить правило авторизации ("ability", "возможность"), используя класс Illuminate\Auth\Access\Gate. AuthServiceProvider, который идет с Laravel - подходящее для этого место.

Давайте, например, определим правило update-post, которое будет определять, имеет ли заданный пользователь возможность редактировать заданный пост. На вход оно будет принимать, очевидно, модели User и Post:

<?php

namespace App\Providers;

use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application authentication / authorization services.
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function boot(GateContract $gate)
    {
        $this->registerPolicies($gate);

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

Если операция разрешена (в нашем случае если id пользователя совпадает с user_id поста), то операцию разрешаем, возвращая true. Если запрещена - правило должно возвращать false.

Заметьте, что мы не проверяем, залогинен ли пользователь, существует ли объект $user. Laravel проверяет это автоматически и возвращает false для всех правил, где объект User не определён, включая метод forUser.

Определение метода в классе

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

$gate->define('update-post', 'Class@method');

Перехват проверки авторизации

Иногда вам нужно дать некому пользователю (например, админу) права на все операции. Для этого можно воспользоваться методом before, который запускается перед всеми остальными проверками:

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

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

You may use the after method to define a callback to be executed after every authorization check. However, you may not modify the result of the authorization check from an after callback:

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

$gate->after(function ($user, $ability, $result, $arguments) {
    //
});

Проверка правил

при помощи фасада Gate

Как только правило определено, мы можем проверить его на соответствие. Для этого мы можем воспользоваться методами check, allows и denies фасада Gate. Эти методы принимают на вход имя правила и аргументы, которые должны быть переданы правилу для обработки. Нет необходимости явно передавать текущего юзера - если среди аргументов есть объект модели User, фреймворк сам подставит туда экземпляр модели залогиненого в данный момент юзера.

<?php

namespace App\Http\Controllers;

use Gate;
use App\User;
use App\Post;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * Update the given post.
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $post = Post::findOrFail($id);

        if (Gate::denies('update-post', $post)) {
            abort(403);
        }

        // Update Post...
    }
}

Метод denies() возвращает true, если операция запрещена, иначе false. Метод allows() позвращает true, если операция разрешена, иначе false. Метод check() является алиасом метода allows(), делает то же самое.

Проверка правил для определённых пользователей

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

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

Передача нескольких аргументов

Если в правило вам нужно передать не два аргумента (пользователь и некая сущность), а несколько, то в определении правила перечислите аргументы обычным способом:

Gate::define('delete-comment', function ($user, $post, $comment) {
    //
});

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

if (Gate::allows('delete-comment', [$post, $comment])) {
    //
}

при помощи модели User

Другой способ проверить правила - воспользоваться методами can и cannot класса User. Эти методы появляются, если подключить к классу трейт Authorizable и они идентичны методам allows() и denies() класса Gate. Вот предыдущий пример, переписанный с использованием этих методов:

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * Update the given post.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return Response
     */
    public function update(Request $request, $id)
    {
        $post = Post::findOrFail($id);

        if ($request->user()->cannot('update-post', $post)) {
            abort(403);
        }

        // Update Post...
    }
}

А вот так выглядит секция проверки с использованием метода can():

if ($request->user()->can('update-post', $post)) {
    // Update Post...
}

внутри шаблонов Blade

Для проверки правил для текущего залогиненого пользователя в шаблонах Blade можно использовать директивe @can:

<a href="/post/{{ $post->id }}">View Post</a>

@can('update-post', $post)
    <a href="/post/{{ $post->id }}/edit">Edit Post</a>
@endcan

You may also combine the @can directive with @else directive:

@can('update-post', $post)
    <!-- The Current User Can Update The Post -->
@else
    <!-- The Current User Can't Update The Post -->
@endcan

при валидации запросов

Проверку правил авторизации можно проводить в процессе валидации запросов, в методе authorize(). Делать это можно при помощи класса Gate:

/**
 * Determine if the user is authorized to make this request.
 *
 * @return bool
 */
public function authorize()
{
    $postId = $this->route('post');

    return Gate::allows('update', Post::findOrFail($postId));
}

Политики

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

Прописывать правила для каждой сущности в AuthServiceProvider может быть утомительно, а в случае большого приложения - еще и слабочитаемо. Для наведения порядка в этой области Laravel предлагает так называемые классы политик (policies). В них правила доступа сгруппированы по каждой сущности.

Например, сделаем класс политики для сущности Post. Для создания класса воспользуемся артизан-командой make:policy. Она создаст класс политики в папке app/Policies:

php artisan make:policy PostPolicy

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

Как только класс политики создан, его надо зарегистрировать. Для этого добавьте его в свойство policies класса AuthServiceProvider вместе с сущностью, доступ к которой он призван регулировать:

<?php

namespace App\Providers;

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

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

    /**
     * Register any application authentication / authorization services.
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function boot(GateContract $gate)
    {
        $this->registerPolicies($gate);
    }
}

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

Теперь, после создания и регистрации класса политик, мы можем приступить к написанию правил авторизации. Давайте опишем правило update, которое будет определять имеет залогиненый пользователь права для редактирования поста, или нет.

<?php

namespace App\Policies;

use App\User;
use App\Post;

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

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

Прерывание всех проверок

Иногда вам нужно дать для некого пользователя (админа) разрешение на все права в данном классе политики. Для этого используйте метод before:

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

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

Проверка политик

Проверка правил классов политик фактически не отличается от проверки правил, заданных в функциях-замыканиях. Вы можете использовать фасад Gate, метод can() модели User, директиву Blade @can. Плюс вы можете использовать специальный хелпер для класса политик.

при помощи фасада Gate

Фасад Gate автоматичести распознает тип объекта, который подается в качестве аргумента, и выбирает соответствующий класс политик, если таковой зарегистрирован. Имя правила для проверки, которое подается первым аргументом - это название метода класса политик, который будет вызывать Gate.

<?php

namespace App\Http\Controllers;

use Gate;
use App\User;
use App\Post;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * Update the given post.
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $post = Post::findOrFail($id);

        if (Gate::denies('update', $post)) {
            abort(403);
        }

        // Update Post...
    }
}

при помощи модели User

То же самое работает в отношении методов can() и cannot() модели User. Если для объекта, который указывается во втором аргументе, зарегистрирован класс политики, правило для проверки (метод класса политики) берется оттуда.

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

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

внутри шаблонов Blade

То же самое работает в отношении директивы Blade @can. Если для объекта, который указывается во втором аргументе, зарегистрирован класс политики, правило для проверки (метод класса политики) берется оттуда.

@can('update', $post)
    <!-- The Current User Can Update The Post -->
@endcan

при помощи хелпера политики

При помощи глобального хелпера policy() можно получить инстанс класса политики, связанного с заданным объектом (в нешам случае - с объектом $post) и вызвать метод проверки правила авторизации напрямую:

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

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

Класс App\Http\Controllers\Controller, от которого по умолчанию наследуются все контроллеры приложения, имеет трейт AuthorizesRequests. Этот трейт предоставляет метод authorize(), который может использоваться для проверки правила авторизации и выброса эксепшна AuthorizationException в случае неудачи.

Метод authorize() используется так же, как Gate::allows и $user->can(), т.е. проверяет, возвращает ли правило true.

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * Update the given post.
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $post = Post::findOrFail($id);

        $this->authorize('update', $post);

        // Update Post...
    }
}

В данном примере если действие разрешается, выполнение кода продолжается дальше. Если же правило вернуло false, то бросается исключение AuthorizationException, которое генерирует http-ответ с кодом ошибки 403 ("Not Authorized").

Также в трейте AuthorizesRequests есть метод authorizeForUser(), который осуществляет проверку правила авторизации не по отношению к залогиненному юзеру, а для произвольно заданного:

$this->authorizeForUser($user, 'update', $post);

Автоматическое определение метода класса политики

Часто название правила авторизации совпадает с названием метода контроллера. Например, в нашем примере они оба имеют имя "update". Для таких случаев в Laravel предусмотрено такое поведение - если в authorize() не передается строка названия метода, то берется название метода контроллера, из которого вызывается authorize():

/**
 * Update the given post.
 *
 * @param  int  $id
 * @return Response
 */
public function update($id)
{
    $post = Post::findOrFail($id);

    $this->authorize($post);

    // Update Post...
}

Полный цикл в данном случае выглядит так: Laravel смотрит, какой класс политики зарегистрирован для объекта типа $post (это в нашем случае ), далее смотрит название текущего метода метода контроллера, и вызывает метод update класса PostPolicy