Гейты и политики в Laravel: где ломается авторизация
Gate::allows возвращает true для гостя, политика не подхватывается, а before() открывает всё админу. Разбираем, как устроена авторизация в Laravel и где она даёт сбой на живом проекте.

Классическая история: контроллер защищён $this->authorize('update', $post), тесты зелёные, а на проде пользователь редактирует чужой пост. Причина не в бизнес-логике, а в том, что политика не зарегистрировалась, и Laravel молча пропустил проверку. Авторизация в Laravel устроена из двух слоёв — гейты и политики, — и большинство утечек прав возникает на стыке этих слоёв, а не внутри самих правил. Разберём, как этот механизм работает под капотом и в каких местах он ведёт себя не так, как ожидает разработчик.
Материал написан для Laravel 10.x и 11.x. Различия между ними в части авторизации минимальны, но одно важное изменение (автоматическое обнаружение политик) отмечу отдельно.
Два слоя: гейт и политика
Gate — это глобальный реестр замыканий, зарегистрированных по строковому имени. Политика — это класс, привязанный к модели, где каждый метод отвечает за одно действие. Оба слоя резолвятся через один и тот же сервис Illuminate\Auth\Access\Gate. Когда вы вызываете Gate::allows('update-post', $post), фреймворк ищет замыкание с таким именем. Когда вызываете $user->can('update', $post), он смотрит на класс модели $post, находит зарегистрированную политику и вызывает её метод update.
Ключевой момент, который упускают: если ни гейт, ни политика для действия не найдены, результат по умолчанию — отказ. Но есть исключение — метод authorizeResource и Blade-директива @can, которые ведут себя тише, чем ожидаешь. Об этом ниже.
Как политика привязывается к модели
В Laravel 11 включено автообнаружение: для модели App\Models\Post фреймворк ищет App\Policies\PostPolicy. Если ваши классы лежат нестандартно (например, модель в App\Domain\Blog\Post), автообнаружение не сработает и проверка молча провалится в отказ. В таком случае регистрируйте политику явно:
// app/Providers/AppServiceProvider.php, Laravel 11.x
use App\Domain\Blog\Post;
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::policy(Post::class, PostPolicy::class);
}В Laravel 10 автообнаружение тоже есть, но многие проекты до сих пор держат явную карту в AuthServiceProvider через свойство $policies. Смешивать явную регистрацию и автообнаружение можно — явная имеет приоритет.
Где авторизация ломается первой
Ниже — реальные сбои, которые не ловятся глазами при чтении кода и всплывают только под конкретным пользователем.
1. Политика не найдена — молчаливый отказ или молчаливый пропуск
Если модель и политика лежат не по конвенции, $user->can('update', $post) вернёт false всегда. Разработчик видит «403» и уходит чинить права, хотя чинить надо регистрацию. Обратная сторона: если вы проверяете гейт по строке, которой не существует (Gate::allows('edit-psot', $post) с опечаткой), тоже получите отказ — и легко списать это на баг в правиле.
2. before() перекрывает всё
Метод before в политике вызывается до любого другого метода. Типичный код для суперадмина:
// PostPolicy.php
public function before(User $user, string $ability): ?bool
{
if ($user->isSuperAdmin()) {
return true;
}
return null; // важно: null, а не false
}Ошибка здесь — вернуть false вместо null в ветке «не суперадмин». false — это явный отказ для всех действий, и остальные методы политики не вызовутся никогда. Возвращайте null, чтобы передать управление дальше. Ещё тоньше: before перехватывает и те действия, которые вы намеренно хотели запретить даже админу (например, delete оплаченного заказа). Держите такие исключения в самом before.
3. Гость проходит проверку
Когда пользователь не аутентифицирован, Auth::user() возвращает null. По умолчанию Laravel не вызывает методы политики для гостя и сразу отказывает. Но если вы объявили аргумент как nullable — public function view(?User $user, Post $post) — гейт вызовет метод и передаст null. Это нужно для публичных действий, но опасно: забыли обработать $user === null внутри — и гость получает права авторизованного.
Правило, которое экономит часы отладки: если метод политики принимает ?User $user, первая строка метода обязана явно решать судьбу гостя. Нет обработки null — значит, вы не думали о госте, а он к вам придёт.4. authorizeResource и скрытые имена действий
Вызов $this->authorizeResource(Post::class, 'post') в конструкторе контроллера автоматически привязывает методы контроллера к методам политики: index → viewAny, show → view, store → create, update → update, destroy → delete. Если в политике нет метода viewAny, а контроллер вызвал index, проверка провалится в отказ — и вы получите 403 на списке, не понимая почему. Имена методов политики должны точно совпадать с ожидаемыми.
Гейт или политика: что выбирать
| Критерий | Gate (замыкание) | Policy (класс) |
|---|---|---|
| Привязка к модели | Нет, любое действие | Одна модель — одна политика |
| Число правил | 1–2 разрозненных | Много действий над сущностью |
| Пример | access-admin-panel | CRUD над Post |
| Где регистрировать | Gate::define() | Автообнаружение или Gate::policy() |
| Тестируемость | Сложнее изолировать | Обычный класс, легко мокать |
Практический критерий: если действие относится к конкретной модели и таких действий больше одного — политика. Если это глобальное разрешение вроде «видеть страницу настроек» или «экспортировать отчёт», не привязанное к записи, — гейт.
Как проверять авторизацию в коде
Способов несколько, и они дают разное поведение при отказе:
$this->authorize('update', $post)в контроллере — бросаетAuthorizationException, превращается в HTTP 403;Gate::allows('update', $post)и$user->can(...)— возвращаютbool, ничего не бросают, решение за вами;- middleware
can:update,postна маршруте — проверка до входа в контроллер, отказ = 403; @can('update', $post)в Blade — показывает или скрывает блок, но не защищает эндпоинт.
Последний пункт — источник дыр. Скрыть кнопку «Удалить» через @can недостаточно: если пользователь знает URL, запрос уйдёт. Blade-директива — только про интерфейс, серверную проверку всё равно нужно ставить в контроллере или middleware.
Response вместо голого bool
Метод политики может вернуть не только bool, но и Response с текстом причины отказа — это полезно, когда фронту нужно объяснить, почему нельзя:
use Illuminate\Auth\Access\Response;
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('Редактировать можно только свои записи.');
}Сообщение из Response::deny() попадёт в текст AuthorizationException и будет доступно на фронте.
Что мерить и на что смотреть в проде
Авторизация редко становится узким местом по скорости, но метод before, дёргающий базу на каждой проверке (например, загрузка ролей пользователя без кеша), под нагрузкой даёт лишние запросы. Если на одной странице вы вызываете @can в цикле по коллекции из 200 записей, а политика на каждой итерации подтягивает связи — это N+1 внутри авторизации, который не виден в стандартном профайлере запросов контроллера.
- логируйте
AuthorizationExceptionотдельно от прочих 403 — всплеск означает либо баг в регистрации политики, либо перебор доступа; - проверяйте, что каждый защищённый маршрут имеет серверную проверку, а не только
@canв шаблоне; - под нагрузкой следите за числом запросов к таблице ролей/разрешений на один HTTP-запрос.
Что дальше
Следующий шаг — покрыть политики тестами: в Laravel это делается через $this->actingAs($user)->assertForbidden() и прямой вызов методов политики как обычного класса. Если ролей и разрешений становится много, посмотрите в сторону пакета spatie/laravel-permission — он не заменяет гейты и политики, а даёт слой хранения ролей в базе, поверх которого вы всё равно пишете политики. Инвалидация кеша ролей — отдельная тема, здесь её не разбираем.
Оформление по ГОСТ: Поля, шрифты, нумерация, сноски, титульный лист
Спросить за 10 ₽Источники: Laravel Documentation — Authorization
Частые вопросы
Политика не срабатывает, хотя я её написал — с чего начать?
У меня другая версия Laravel — 9 или ниже. Что изменится?
Почему before() открыл доступ ко всему, хотя я запретил конкретное действие?
Достаточно ли скрыть кнопку через @can, чтобы защитить действие?
Как разрешить действие гостю, но не открыть его случайно всем?
В чём разница между authorize(), Gate::allows() и can на маршруте?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.