Авторизация в 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();
}
}