<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
LARAVEL · MIDDLE ЧАСТО ПИТАЮТЬ

Як влаштована авторизація в Laravel: Gates, Policies, middleware, Form Request?

Усі чотири механізми - це один `Gate`: `Gate::authorize()` шукає policy за першим аргументом (`App\Models\Post` -> `App\Policies\PostPolicy`), інакше падає на іменовану здібність з `Gate::define()`, а `can` middleware, `$this->authorize()`, `authorize()` у Form Request і `@can` у Blade лише викликають його в різних точках запиту. Відмова кидає `AuthorizationException` -> 403, а `Response::denyAsNotFound()` перетворює її на 404, коли сам факт існування запису розголошувати не можна.

Користувач відкриває чужу неопубліковану статтю і отримує 403. Чому це витік і що треба віддати замість нього?
У `PostPolicy` є `before()`, який пускає адміна всюди, але на `restore` адмін усе одно ловить 403. Де помилка?
`authorize()` у Form Request повернув `false` - клієнт побачить 403 чи 422, і чи виконаються `rules()`?
Параметр маршруту перейменували з `{post}` на `{article}`, а в middleware лишилось `can:update,post`. Що тепер станеться з правами?
Gate Policy авторизація 403 Form Request middleware

Чотири назви з питання описують один механізм, до якого дотягуються з різних точок запиту. Будь-яка перевірка врешті доходить до Gate::raw(), і порядок там фіксований: спершу глобальні колбеки Gate::before() (перший не-null результат обриває все інше), потім спроба знайти policy за першим аргументом, потім іменована здібність з Gate::define(), і вже на виході Gate::after(). Policy знаходиться без реєстрації: guessPolicyName() бере клас моделі, міняє сегмент \Models\ на \Policies\ і додатково перебирає неймспейс угору, тому App\Models\Post дає App\Policies\PostPolicy. Зі структурою поза стандартом (модулі, DDD, моделі поза App\Models) конвенція не спрацьовує, і тоді є три виходи: атрибут #[UsePolicy(PostPolicy::class)] на моделі, явний Gate::policy() або власне правило через Gate::guessPolicyNamesUsing().

Найчастіше валяться на наступному кроці. resolvePolicyCallback() спочатку перевіряє, чи взагалі є в policy метод під цю здібність, і якщо ні, повертає false, тобто Gate іде шукати іменовану здібність. Немає і її - відмова, 403, жодного винятку й жодного запису в логах. Звідси класична загадка «policy є, метод є, а прав немає»: здібність називається update, а метод хтось назвав edit. З того ж факту випливає поведінка before() у самій policy: він викликається всередині вже побудованого policy-колбека, тому для здібності без відповідного методу не спрацює. Адмінський обхід, який має покривати справді все, живе в глобальному Gate::before(), а не в policy. Гості - окрема історія: метод із типом User $user для null не викликається взагалі, результат null перетворюється на deny, і користувач без сесії отримує 403 замість пропозиції залогінитись. Хочете пускати гостя в перевірку - пишіть ?User $user.

can:update,post - це аліас Illuminate\Auth\Middleware\Authorize, і працює він до створення контролера. Аргумент він розбирає просто: рядок зі зворотним слешем вважається іменем класу (can:create,App\Models\Post для create і viewAny, де моделі ще немає), решта шукається серед параметрів маршруту. Не знайшлося - у Gate піде null, policy за null не знайдеться, і ендпоїнт почне віддавати 403 усім підряд; перейменування {post} на {article} без правки middleware дає рівно цей ефект. Для ресурсних контролерів усе це розвішує authorizeResource(Post::class, 'post') за мапою index -> viewAny, show -> view, create/store -> create, edit/update -> update, destroy -> delete. Метод $this->authorize() у контролері потребує трейта AuthorizesRequests, якого в порожньому базовому Controller з Laravel 11+ уже немає: або додайте трейт, або викликайте Gate::authorize().

Form Request зводить доступ і валідацію в один клас, і тут головне - порядок. validateResolved() робить prepareForValidation(), далі passesAuthorization(), і лише потім будує валідатор. Звідси два наслідки. Відмова в доступі дає 403, а rules() не виконуються, тому в тестах, де очікували 422 зі списком полів, ви побачите порожню відмову. І навпаки, перевіряти можна по тілу запиту, бо дані вже під рукою: дозволити зміну status на published тільки редактору, а решту полів лишити автору. Модель у маршруті на цей момент уже підставлена біндингом, тож $this->route('post') віддає обʼєкт, а 404 для неіснуючого id трапився раніше. Побутове: make:request у Laravel 11-13 генерує authorize(): bool { return false; }, і забутий return true виглядає як незрозумілий 403 на кожну відправку форми; а $this->user()->can(...) без auth у маршруті - це 500 на null. Якщо ж метод authorize() у класі просто не оголошений, passesAuthorization() пропускає запит далі, бо перевіряє його наявність через method_exists().

@can('update', $post) у Blade компілюється у виклик Gate::check() і вирішує єдине питання: показувати кнопку чи ні. Захисту він не дає, і саме про це питають «з підступом» - сховали кнопку, забули policy на PUT, отримали IDOR. Зі списками policy теж не допоможе: вона відповідає на питання про конкретний обʼєкт, а index треба звужувати запитом (->where('author_id', $user->id) або скоуп тенанта). Останнє, за що ставлять плюс, - усвідомлений вибір коду відповіді. 403 повідомляє співрозмовнику, що запис існує, просто він не ваш; для чужих чернеток, приватних профілів і чужих тенантів це підказка, яка дозволяє перебирати id, тому policy повертає Response::denyAsNotFound() (обробник винятків бачить статус у AuthorizationException і віддає 404), або модель узагалі не знаходиться - через вибірку від користувача чи Route::scopeBindings(). А для колеги в тій самій компанії, якому не вистачає ролі, чесніший 403 з текстом: 404 тут перетворює зрозумілу відмову на звернення в підтримку про «зламаний сайт».

// App\Models\Post -> App\Policies\PostPolicy знаходиться без реєстрації.
final class PostPolicy
{
    // Викликається лише для здібностей, під які в policy є метод.
    public function before(User $user, string $ability): ?bool
    {
        return $user->is_admin ? true : null; // null -> перевірка йде далі
    }

    // ?User: метод спрацює і для гостя. З типом User гість отримав би deny.
    public function view(?User $user, Post $post): Response
    {
        return $post->is_published || $post->author_id === $user?->id
            ? Response::allow()
            : Response::denyAsNotFound(); // 404: чужої чернетки для вас не існує
    }

    public function update(User $user, Post $post): Response
    {
        return $post->author_id === $user->id
            ? Response::allow()
            : Response::deny('Редагувати можна лише власні статті.'); // 403 з текстом
    }
}

final class UpdatePostRequest extends FormRequest
{
    // Виконується перед rules(): відмова -> 403, валідація не запускається.
    public function authorize(): bool
    {
        // Модель уже підставлена біндингом, тож 404 для неіснуючого id стався раніше.
        return $this->user()->can('update', $this->route('post'));
    }

    /** @return array<string, ValidationRule|array<mixed>|string> */
    public function rules(): array
    {
        return ['title' => ['required', 'string', 'max:120']];
    }
}

// auth перед can: інакше гість отримає 403 замість редіректу на логін.
// 'post' має точно збігатися з іменем параметра маршруту.
Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware(['auth', 'can:update,post']);
Що точка входу одна: `Gate::raw()` спершу проганяє `Gate::before`, потім - якщо перший аргумент має policy - метод policy, і лише далі іменовану здібність з `Gate::define()`; тому за наявності `PostPolicy` гейт `update` для моделі `Post` просто не отримає керування.
Що policy знаходиться за конвенцією неймспейсів (`App\Models\Post` -> `App\Policies\PostPolicy`), а для модульної структури є `#[UsePolicy(...)]` на моделі, `Gate::policy()` і `Gate::guessPolicyNamesUsing()`.
Що метод policy з типом `User $user` для гостя взагалі не викликається: результат `null` перетворюється на deny і дає 403, а не редірект на логін - тому `auth` має стояти в ланцюжку раніше за `can`.
Що `authorize()` у Form Request виконується після `prepareForValidation()` і перед побудовою валідатора, тож відмова дає 403 і жодного поля з `rules()` перевірено не буде.
Що `can:update,post` бере з маршруту параметр із таким самим іменем, а рядок зі зворотним слешем (`can:create,App\Models\Post`) передається як клас - для `viewAny`/`create`, де моделі ще немає.
Що вибір між 403 і 404 - це рішення про розголошення: 403 підтверджує, що запис існує, а `Response::denyAsNotFound()` або звуження вибірки (`$request->user()->posts()->findOrFail()`) не підтверджує нічого.
Вважати `@can('update', $post)` захистом: Blade ховає кнопку, а POST на той самий URL проходить, якщо в маршруті чи Form Request перевірки немає.
Назвати метод policy `edit` і перевіряти `update`: `resolvePolicyCallback()` мовчки повертає `false`, Laravel падає на гейти, гейта немає - і виходить стабільний 403 без жодної помилки в логах.
Ставити адмінський обхід у `before()` policy і думати, що він покриває всі здібності: `before()` викликається всередині того самого замикання, яке будується лише коли метод під цю здібність існує; для `restore` без методу `restore()` він не спрацює. Глобальний `Gate::before()` - спрацює.
Забути, що `make:request` у Laravel 11-13 генерує `authorize(): bool { return false; }` - форма віддає 403 на будь-який валідний запит, і півгодини шукають причину у валідації.
Писати в Form Request `$this->user()->can(...)` без `auth` у маршруті: для гостя це `Call to a member function can() on null`, тобто 500 замість 401.
Помилитись в імені параметра в `can:update,post`: middleware не знайде його в маршруті, передасть `null`, policy за `null` не знайдеться, і всі отримають 403 незалежно від прав.
Прикрити policy тільки `show`/`update` і лишити `index`, який віддає чужі записи: policy перевіряє доступ до обʼєкта, а не фільтрує вибірку - для списків потрібні скоупи в запиті.
Покладатись на 404 як на єдиний захист і забути, що сусідній ендпоїнт (експорт, вкладений ресурс, API) віддає ту саму модель без перевірки.
ПОРАДА

Скажіть, що Gates, Policies, middleware і Form Request - це не чотири механізми, а одна перевірка в чотирьох місцях запиту, і назвіть ці місця по порядку: маршрут (`can`), резолв Form Request (`authorize()`), тіло контролера (`Gate::authorize()`), шаблон (`@can` - лише для вигляду). Другим ходом додайте те, про що зазвичай не питають прямо: 403 повідомляє, що обʼєкт існує, тому для чужих чернеток є `Response::denyAsNotFound()`.

оновлено 20 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

`validateResolved()` викликає `prepareForValidation()`, потім `passesAuthorization()`, і лише після цього створює валідатор - тому при `false` клієнт бачить 403, а не 422 зі списком полів. `@can` компілюється у звичайну перевірку `Gate::check()` всередині шаблона і на HTTP-запит не впливає. Відсутній метод policy - це `false` у `resolvePolicyCallback()`, після чого Gate шукає іменовану здібність і, не знайшовши, віддає deny без будь-якого винятку про метод. Гість у `can` не редіректиться: метод policy з типом `User $user` для `null` не викликається, результат перетворюється на deny, тобто 403 - за редірект відповідає `auth`.