Политики доступа в Laravel: рабочие приёмы и типичные ошибки
Gate::allows проверяет одно, метод контроллера — другое, а Blade показывает кнопку тому, кто не должен её видеть. Разбираем, как выстроить авторизацию в Laravel так, чтобы правила не расползались по проекту.

Классическая ситуация: пользователь заходит на страницу редактирования чужого поста, ловит 403 — и тут же видит в списке кнопку «Удалить», потому что в Blade забыли повторить проверку. Или наоборот: контроллер пускает по middleware('auth'), а сам факт владения ресурсом никто не проверяет, и любой залогиненный правит любые записи. Авторизация в Laravel разъезжается не из-за нехватки инструментов, а из-за того, что одна и та же проверка живёт в трёх местах и рассинхронизируется.
Речь про Laravel 10.x и 11.x — механика Gates и Policies в этих версиях совпадает. Разберём, где держать правила, как не дублировать их между контроллером, Blade и API, и что ломается на практике.
Gate или Policy: где что уместно
В Laravel два механизма авторизации, и они не конкурируют, а закрывают разные случаи.
Gate — это именованная проверка, не привязанная к модели. Подходит для действий уровня приложения: «может ли пользователь зайти в админку», «может ли смотреть отчёты». Регистрируется в AppServiceProvider (или в отдельном провайдере) через замыкание.
Policy — класс, где каждый метод отвечает за действие над конкретной моделью: view, create, update, delete. Именно сюда просится логика «владелец поста может его редактировать».
Практическое правило: если проверка звучит как «может ли X сделать Y с этой записью» — это Policy. Если «может ли X вообще делать Y» — это Gate. Не тащите проверку владения ресурсом в Gate: там нет модели, и вы кончите тем, что будете передавать id вручную.
Как выглядит Policy на практике
Генерируется командой, привязанной к модели:
php artisan make:policy PostPolicy --model=PostМетод получает текущего пользователя и экземпляр модели. Laravel сам передаёт их, когда вы вызываете авторизацию:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
// Редактировать может автор поста
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
// Удалять — автор или админ
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id
|| $user->is_admin;
}
}В контроллере вызов один — и он же бросит 403, если проверка не прошла:
public function update(Request $request, Post $post)
{
// Ищет PostPolicy::update автоматически по типу $post
$this->authorize('update', $post);
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}Одна проверка — один источник правды
Главная беда авторизации — дублирование. Правило «автор или админ» переписывают в контроллере, потом руками в Blade, потом в API-ресурсе. Меняется одно — забывают про два других.
Решение — держать правило только в Policy, а во всех местах вызывать его.
В контроллере
Три способа, все ведут к одному Policy-методу:
$this->authorize('update', $post)— из метода контроллера, бросаетAuthorizationException(403);Gate::authorize('update', $post)— то же самое вне контроллера;- middleware
can:прямо в определении маршрута — проверка до входа в метод.
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');В Blade
Не пишите @if($user->id === $post->user_id). Вызывайте тот же метод политики через директиву @can:
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Редактировать</a>
@endcanТеперь при смене правила меняется один метод в PostPolicy, и кнопка, и контроллер, и middleware начинают вести себя одинаково.
В API-ответе
Часто фронтенду нужно знать, что пользователю разрешено, чтобы не рисовать лишние кнопки. Отдавайте это флагом в ресурсе, вычисляя через ту же политику:
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'can' => [
'update' => $request->user()?->can('update', $this->resource),
'delete' => $request->user()?->can('delete', $this->resource),
],
];
}Что учесть заранее
before для суперадмина
Метод before в политике выполняется до всех остальных и может разрешить всё разом. Удобно для роли, которой можно всё, но у него есть ловушка: если before возвращает false, он запрещает действие целиком, минуя обычные методы. Возвращайте true или null, но не false, если не хотите заблокировать проверку остальным.
public function before(User $user, string $ability): ?bool
{
// Админу можно всё; остальным — обычные методы политики
return $user->is_admin ? true : null;
}Гостевые пользователи
Если действие должно быть доступно неавторизованным (например, view публичного поста), типизируйте пользователя как nullable, иначе Laravel не вызовет метод для гостя и вернёт 403:
public function view(?User $user, Post $post): bool
{
return $post->is_published
|| $user?->id === $post->user_id;
}Response вместо голого bool
Когда нужно объяснить, почему отказано, возвращайте Response::deny() с сообщением — оно попадёт в исключение и в ответ:
use Illuminate\Auth\Access\Response;
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('Редактировать можно только свои посты.');
}Сравнение подходов к вызову
| Способ | Где применять | Что делает при отказе |
|---|---|---|
$this->authorize() | Метод контроллера | Бросает 403 |
middleware can: | Определение маршрута | 403 до входа в метод |
$user->can() | Blade, ресурсы, условия | Возвращает bool, не бросает |
Gate::allows() | Проверки без модели | Возвращает bool |
Что ломается на практике
- Policy не находится. В Laravel 11 автодискавери работает по соглашению об именах и неймспейсах: модель
App\Models\Post→ политикаApp\Policies\PostPolicy. Если структура нестандартная, регистрируйте связку явно черезGate::policy(Post::class, PostPolicy::class). Симптом — проверка всегда возвращает отказ, будто метода нет. - Route model binding не сработал. Если в маршруте параметр называется не так, как ожидает middleware
can:update,post, в политику приедет строка или null вместо модели. Имя параметра в URL и вcan:должны совпадать. - Проверка в контроллере есть, в Blade — нет. Кнопка видна, клик ведёт на 403. Раздражает пользователя и выглядит как баг. Оборачивайте действия в
@can. - Гость получает 403 вместо публичного контента. Забыли nullable-тип у
Userв методеview. - before всё ломает. Вернули
falseдля не-админа вместоnull— заблокировали всю политику для обычных пользователей.
Что делать дальше
Проверьте, нет ли в проекте ручных сравнений $user->id === $model->user_id в контроллерах и шаблонах — это первые кандидаты на вынос в Policy. Настройте отдачу флагов can в API-ресурсах, чтобы фронтенд не дублировал логику. Для сложных прав (роли, разрешения на уровне групп) посмотрите в сторону пакета spatie/laravel-permission — он дружит с Gate и не требует переписывать политики. Инвалидация кеша прав и тестирование политик через actingAs — отдельные темы, здесь мы их не разворачивали.
Инфобез и 152-ФЗ: Аудит политик конфиденциальности, правила сбора cookie.
Спросить за 20 ₽Источники: Laravel Documentation — Authorization
Частые вопросы
Чем Gate отличается от Policy, если оба проверяют доступ?
У меня другая версия Laravel — что-то поменяется?
Почему политика всегда возвращает отказ, будто метода нет?
Как разрешить действие неавторизованным пользователям?
Как избежать дублирования проверки между контроллером и Blade?
Что делать, если before запрещает всё обычным пользователям?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.