HardТеория7 min

Авторизация (Authorization)

Gates, Policies, authorizeResource, директивы @can/@cannot, авторизация через middleware, auto-discovery политик

Авторизация в Laravel определяет, ЧТО аутентифицированный пользователь МОЖЕТ делать. Laravel предоставляет два основных механизма: Gates (замыкания для простых действий) и Policies (классы для действий, привязанных к моделям).

Gates

Gates -- это замыкания, определяющие, может ли пользователь выполнить определённое действие. Обычно определяются в AppServiceProvider.

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Simple gate
        Gate::define('view-dashboard', function (User $user): bool {
            return $user->is_admin;
        });

        // Gate with model parameter
        Gate::define('update-post', function (User $user, Post $post): bool {
            return $user->id === $post->user_id;
        });

        // Gate with multiple parameters
        Gate::define('update-comment', function (User $user, Post $post, Comment $comment): bool {
            return $user->id === $comment->user_id
                && $post->id === $comment->post_id;
        });

        // Super admin bypass: before() runs before all gates
        Gate::before(function (User $user, string $ability): ?bool {
            if ($user->isSuperAdmin()) {
                return true; // Bypass all authorization checks
            }

            return null; // Fall through to normal gate check
        });

        // After callback (runs after the gate, does NOT override result)
        Gate::after(function (User $user, string $ability, ?bool $result, mixed ...$arguments): void {
            // Log authorization decisions
            \Log::info("Authorization check: {$ability}", [
                'user' => $user->id,
                'result' => $result,
            ]);
        });
    }
}

Использование Gates

use Illuminate\Support\Facades\Gate;

// Check authorization
if (Gate::allows('update-post', $post)) {
    // User can update the post
}

if (Gate::denies('update-post', $post)) {
    // User cannot update the post
}

// For a specific user (not the current user)
if (Gate::forUser($anotherUser)->allows('update-post', $post)) {
    // $anotherUser can update the post
}

// Throws AuthorizationException if denied
Gate::authorize('update-post', $post);

// Check any / all
if (Gate::any(['update-post', 'delete-post'], $post)) {
    // User can update OR delete
}

if (Gate::none(['update-post', 'delete-post'], $post)) {
    // User can neither update nor delete
}

// Inline authorization with response
Gate::inspect('update-post', $post);
// Returns Illuminate\Auth\Access\Response

Gate Response

use Illuminate\Auth\Access\Response;

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

    return Response::deny('Вы не являетесь автором этой статьи.');

    // Or with HTTP status code
    return Response::denyWithStatus(404); // Pretend resource doesn't exist
    return Response::denyAsNotFound(); // Shorthand for 404
});

Policies

Policies -- классы, группирующие логику авторизации вокруг конкретной модели.

Создание Policy

php artisan make:policy PostPolicy --model=Post
<?php

declare(strict_types=1);

namespace App\Policies;

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

final class PostPolicy
{
    /**
     * Perform pre-authorization checks.
     * Runs before any other method.
     */
    public function before(User $user, string $ability): ?bool
    {
        if ($user->isSuperAdmin()) {
            return true;
        }

        return null; // Fall through to specific method
    }

    /**
     * Determine whether the user can view any posts.
     */
    public function viewAny(User $user): bool
    {
        return true; // Everyone can view the list
    }

    /**
     * Determine whether the user can view the post.
     */
    public function view(User $user, Post $post): bool
    {
        // Published posts are visible to everyone
        if ($post->is_published) {
            return true;
        }

        // Drafts only to the author
        return $user->id === $post->user_id;
    }

    /**
     * Determine whether the user can create posts.
     */
    public function create(User $user): bool
    {
        return $user->hasPermission('posts.create');
    }

    /**
     * Determine whether the user can update the post.
     */
    public function update(User $user, Post $post): Response
    {
        if ($user->id === $post->user_id) {
            return Response::allow();
        }

        if ($user->isEditor()) {
            return Response::allow();
        }

        return Response::deny('Вы не можете редактировать эту статью.');
    }

    /**
     * Determine whether the user can delete the post.
     */
    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    /**
     * Determine whether the user can restore the soft-deleted post.
     */
    public function restore(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    /**
     * Determine whether the user can permanently delete the post.
     */
    public function forceDelete(User $user, Post $post): bool
    {
        return $user->isSuperAdmin();
    }
}

Policy Auto-Discovery

Laravel автоматически обнаруживает политики, если они следуют конвенции именования:

  • Модель: App\Models\Post
  • Политика: App\Policies\PostPolicy

Если политика в нестандартном месте, зарегистрируйте вручную:

use Illuminate\Support\Facades\Gate;

// In AppServiceProvider::boot()
Gate::policy(Post::class, PostPolicy::class);

Использование Policies

Через модель User

// The User model has methods from the Authorizable trait
if ($user->can('update', $post)) {
    // User can update the post
}

if ($user->cannot('delete', $post)) {
    // User cannot delete the post
}

// For actions without a model instance (e.g., create)
if ($user->can('create', Post::class)) {
    // User can create posts
}

Через Controller

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class PostController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        // authorize() throws AuthorizationException if denied
        $this->authorize('viewAny', Post::class);

        $posts = Post::all();

        return response()->json($posts);
    }

    public function show(Post $post): JsonResponse
    {
        $this->authorize('view', $post);

        return response()->json($post);
    }

    public function store(Request $request): JsonResponse
    {
        $this->authorize('create', Post::class);

        $post = Post::create($request->validated());

        return response()->json($post, 201);
    }

    public function update(Request $request, Post $post): JsonResponse
    {
        $this->authorize('update', $post);

        $post->update($request->validated());

        return response()->json($post);
    }

    public function destroy(Post $post): JsonResponse
    {
        $this->authorize('delete', $post);

        $post->delete();

        return response()->json(null, 204);
    }
}

authorizeResource

Автоматически назначает авторизацию всем методам ресурсного контроллера.

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Post;

final class PostController extends Controller
{
    public function __construct()
    {
        // Maps controller methods to policy methods:
        // index   -> viewAny
        // show    -> view
        // create  -> create
        // store   -> create
        // edit    -> update
        // update  -> update
        // destroy -> delete
        $this->authorizeResource(Post::class, 'post');
    }

    // Controller methods without manual authorize() calls
    public function index() { /* ... */ }
    public function show(Post $post) { /* ... */ }
    public function store(Request $request) { /* ... */ }
    public function update(Request $request, Post $post) { /* ... */ }
    public function destroy(Post $post) { /* ... */ }
}

Авторизация через Middleware

use App\Models\Post;

// Using gate
Route::get('/dashboard', function () {
    // ...
})->middleware('can:view-dashboard');

// Using policy
Route::put('/posts/{post}', function (Post $post) {
    // ...
})->middleware('can:update,post');

// For actions without model instance
Route::post('/posts', function () {
    // ...
})->middleware('can:create,App\Models\Post');

Авторизация через Form Request

<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;

final class UpdatePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        $post = $this->route('post');

        return $this->user()->can('update', $post);
    }

    /**
     * @return array<string, array<int, string>>
     */
    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'content' => ['required', 'string'],
        ];
    }
}

Blade-директивы

{{-- Check specific ability --}}
@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Редактировать</a>
@endcan

@cannot('delete', $post)
    <p>У вас нет прав на удаление</p>
@endcannot

{{-- With else --}}
@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Редактировать</a>
@elsecan('view', $post)
    <a href="{{ route('posts.show', $post) }}">Просмотр</a>
@endcan

{{-- For actions without model instance --}}
@can('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}">Создать статью</a>
@endcan

{{-- canany - any of the abilities --}}
@canany(['update', 'delete'], $post)
    <div class="actions">
        @can('update', $post) <button>Редактировать</button> @endcan
        @can('delete', $post) <button>Удалить</button> @endcan
    </div>
@endcanany

{{-- Guest check --}}
@auth
    <p>Вы вошли в систему</p>
@endauth

@guest
    <p>Пожалуйста, войдите</p>
@endguest

Авторизация для гостей (Guest Users)

По умолчанию gates и policies автоматически возвращают false для неаутентифицированных пользователей. Можно изменить это, используя nullable type hint.

// In a Gate
Gate::define('view-post', function (?User $user, Post $post): bool {
    // $user may be null (guest)
    if ($post->is_published) {
        return true;
    }

    return $user !== null && $user->id === $post->user_id;
});

// In a Policy
public function view(?User $user, Post $post): bool
{
    if ($post->is_published) {
        return true; // Guests can view published posts
    }

    return $user !== null && $user->id === $post->user_id;
}

Implicit Model Binding в Policies

// When route uses model binding:
Route::put('/posts/{post}', [PostController::class, 'update']);

// Laravel resolves the Post model and passes it to the policy
// The policy parameter name must match the route parameter

Тестирование авторизации

<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Tests\TestCase;

final class PostAuthorizationTest extends TestCase
{
    public function test_author_can_update_own_post(): void
    {
        $user = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $user->id]);

        $response = $this->actingAs($user)
            ->putJson("/api/posts/{$post->id}", [
                'title' => 'Updated Title',
                'content' => 'Updated Content',
            ]);

        $response->assertOk();
    }

    public function test_user_cannot_update_others_post(): void
    {
        $user = User::factory()->create();
        $otherUser = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $otherUser->id]);

        $response = $this->actingAs($user)
            ->putJson("/api/posts/{$post->id}", [
                'title' => 'Hacked Title',
            ]);

        $response->assertForbidden();
    }

    public function test_admin_can_do_anything(): void
    {
        $admin = User::factory()->create(['is_admin' => true]);
        $post = Post::factory()->create();

        $this->assertTrue($admin->can('update', $post));
        $this->assertTrue($admin->can('delete', $post));
        $this->assertTrue($admin->can('forceDelete', $post));
    }

    public function test_guest_can_view_published_posts(): void
    {
        $post = Post::factory()->create(['is_published' => true]);

        $response = $this->getJson("/api/posts/{$post->id}");

        $response->assertOk();
    }
}

Проверь себя

В чём разница между Gate::allows() и Gate::authorize()?

Какое соответствие методов контроллера и политики создаёт authorizeResource()?

Что произойдёт, если метод after() в Gate вернёт true, когда конкретный gate вернул false?

Как разрешить неаутентифицированному пользователю (гостю) проходить проверку Gate или Policy?

Что возвращает метод before() в Policy, если он возвращает null?