LaravelでWebアプリケーションや管理画面を開発する際、ログイン機能(認証:Authentication)を実装した次に必ず直面するのが「誰がどの操作を行えるか」というアクセス権限管理(認可:Authorization)の実装です。
Laravelには標準で強力な認可機能として Gate(ゲート) と Policy(ポリシー) の2つの仕組みが提供されています。しかし、
- 「GateとPolicyは何が違って、どう使い分ければいいの?」
- 「ControllerやBladeテンプレートで権限判定を行う正しい方法は?」
- 「管理者ユーザーにすべての権限を自動付与したい場合はどう書く?」
といった疑問や迷いを抱えているエンジニアの方も多いのではないでしょうか。
- 認可(Authorization)と認証(Authentication)の根本的な違い
- Gate と Policy の決定的な違いと使い分け早見表
- Gate の定義・判定方法と管理者スーパーバイザー(全権限付与)の実装
- Policy の作成・CRUDアクションの認可ロジックと自動検出(Auto-Discovery)
- Controller・Blade・Routeミドルウェア・FormRequestでの5つの実践判定パターン
- 現場でありがちなアンチパターンとトラブルシューティング
この記事を読めば、Laravelにおける認可設計の全体像が掴め、チーム開発でも迷わない美しく保守性の高い権限管理が実装できるようになります!
1. 認証(Authentication)と認可(Authorization)の違い
権限管理の実装に入る前に、まず「認証」と「認可」という2つの概念を明確に区別しておきましょう。
| 概念 | 英語 | 意味・目的 | Laravelでの主な機能・ツール |
|---|---|---|---|
| 認証 | Authentication (Auth) | 「あなたは誰か?」を特定・検証する(ログイン) | Breeze, Sanctum, Fortify, Session |
| 認可 | Authorization (Authz) | 「あなたは何をしてよいか?」を検証・許可する(権限判定) | Gate, Policy, Canミドルウェア |
例えば、ブログシステムにおいて「ログインできること」が認証であり、「ログイン中のユーザーが自分の投稿を編集できるが、他人の投稿は編集できないように制限すること」が認可です。
Laravelでは、認証されたユーザー(auth()->user())を前提として、GateとPolicyという2つの仕組みで認可をエレガントに実装します。
2. GateとPolicyの違い・使い分け早見表
Laravelの認可システムの中核である「Gate」と「Policy」の最大の違いは、「特定のアクション・機能に紐づくか」それとも「Eloquentモデル(リソース)に紐づくか」という点にあります。
| 比較項目 | Gate(ゲート) | Policy(ポリシー) |
|---|---|---|
| 対象の単位 | 汎用的なアクション・機能全般 | 特定のEloquentモデル(リソース) |
| 実装形式 | クロージャ(無名関数)またはクラスメソッド | 専用のクラス(PostPolicy など) |
| 定義場所 | AppServiceProvider::boot()(※Laravel 10以前は AuthServiceProvider) |
app/Policies/PostPolicy.php |
| 適しているケース |
・「管理者画面へのアクセス権」 ・「ベータ機能の利用フラグ」 ・「全サイト一括エクスポート権限」など |
・「記事(Post)の作成・閲覧・更新・削除」 ・「コメント(Comment)の編集」 ・「注文(Order)のキャンセル」など |
| コマンド生成 | なし(サービスプロバイダに記述) | php artisan make:policy PostPolicy --model=Post |
・特定のEloquentモデルに対するCRUD操作(作成・編集・削除など) ➔ Policy を使う。
・モデルに紐づかないシステム全体の権限・画面アクセス・機能フラグ ➔ Gate を使う。
※実際のWeb開発現場では、モデル操作が多いため「大半の認可はPolicyで管理し、管理画面アクセスなど一部の汎用権限のみGateで管理する」という構成が最も綺麗で保守しやすくなります。
3. Gate(ゲート)の定義と使い方
Gateは、ルートやコントローラのアクションとは独立した、シンプルなクロージャベースの認可機能です。ルート定義のクロージャに似た感覚で直感的に記述できます。
3-1. Gateの定義場所と基本構文
Laravel 11以降では、app/Providers/AppServiceProvider.php の boot() メソッド内に Gate::define() を記述します。
(※Laravel 10以前は app/Providers/AuthServiceProvider.php 内に定義します)
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
// 管理者権限を持つユーザーかどうかを判定するGate
Gate::define('access-admin-panel', function (User $user) {
return $user->role === 'admin';
});
// プレミアム会員かどうかを判定するGate
Gate::define('access-premium-content', function (User $user) {
return $user->is_premium === true;
});
}
}
第1引数にGateの名前(権限名)、第2引数にクロージャを渡します。クロージャの第1引数には現在ログインしている User モデルのインスタンスが自動的に注入されます。
3-2. パラメータを受け取るGateの定義
Gateには、追加の引数(モデルインスタンスやIDなど)を渡して判定を行うことも可能です。
use App\Models\User;
use App\Models\Post;
use Illuminate\Support\Facades\Gate;
// 記事の更新権限判定(追加でPostモデルを受け取る)
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
3-3. 管理者への全権限付与:Gate::before と Gate::after
Webシステムでは「システム管理者(Super Admin)にはすべての操作を無条件で許可したい」という要件がよくあります。
このような場合は、Gate::before() コールバックを使用します。
// app/Providers/AppServiceProvider.php
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
// すべてのGate・Policy判定の「前」に実行される
Gate::before(function (User $user, string $ability) {
if ($user->is_super_admin) {
return true; // trueを返すと以降の個別判定をスキップして即時認可
}
// nullを返すと、個別のGate/Policyの判定処理に進む
return null;
});
}
false を返さないこと!Gate::before で false を返してしまうと、以降のすべての個別認可判定が無条件で拒否(403)されてしまいます。スーパー管理者に該当しない場合は必ず null(または何も返さない)ようにしてください。
3-4. Gateの判定メソッド一覧
PHPコード(コントローラやサービスクラスなど)内でGateをチェックするには、Gate ファサードの各種メソッドを使用します。
use Illuminate\Support\Facades\Gate;
// 1. allows: 権限があるか(boolean: true/false)
if (Gate::allows('access-admin-panel')) {
// 管理者画面を表示
}
// 2. denies: 権限がないか(allowsの逆)
if (Gate::denies('access-admin-panel')) {
abort(403, '管理者権限がありません。');
}
// 3. authorize: 権限がなければ自動的に 403 HttpException をスロー
Gate::authorize('access-admin-panel');
// 4. 引数付きの判定
if (Gate::allows('update-post', $post)) {
// 編集処理
}
// 5. 複数権限の判定
if (Gate::any(['edit-post', 'delete-post'], $post)) {
// 編集または削除のいずれかの権限がある場合
}
if (Gate::none(['edit-post', 'delete-post'], $post)) {
// どちらの権限もない場合
}
4. Policy(ポリシー)の作成・定義と使い方
Policyは、特定のEloquentモデル(リソース)に対する認可ロジックを1つのクラスにまとめる機能です。
モデルごとにコントローラを作成するように、モデルごとにPolicyを作成するのがLaravelの標準アーキテクチャです。
4-1. make:policy コマンドでPolicyクラスを生成
Artisanコマンドを使用して、CRUDアクション用のメソッド(ボイラープレート)が含まれたPolicyクラスを一発生成します。
# Postモデルに対応するPolicyを作成
php artisan make:policy PostPolicy --model=Post
このコマンドを実行すると、app/Policies/PostPolicy.php が生成されます。
4-2. Policyクラスの構造と実装例
生成されたPolicyクラスには、RESTfulコントローラのアクションに対応する標準メソッドがあらかじめ用意されています。
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PostPolicy
{
/**
* 一覧画面の閲覧権限
*/
public function viewAny(User $user): bool
{
return true; // ログインユーザー全員に許可
}
/**
* 個別記事の閲覧権限
*/
public function view(User $user, Post $post): bool
{
// 公開記事、または自分の下書き記事なら閲覧許可
return $post->is_published || $user->id === $post->user_id;
}
/**
* 新規記事の作成権限
*/
public function create(User $user): bool
{
// メール認証済み、または特定のロールを持つユーザーのみ
return $user->email_verified_at !== null;
}
/**
* 記事の更新権限
*/
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->role === 'moderator';
}
/**
* 記事の復元権限(論理削除時)
*/
public function restore(User $user, Post $post): bool
{
return $user->role === 'admin';
}
/**
* 記事の完全削除権限
*/
public function forceDelete(User $user, Post $post): bool
{
return $user->role === 'admin';
}
}
4-3. Policyの自動検出(Auto-Discovery)
LaravelにはPolicyの自動検出(Auto-Discovery)機能が備わっています。
以下の標準的なディレクトリ構成・命名規則に従っていれば、サービスプロバイダへの登録を一切行うことなく自動的にPolicyがモデルと紐づけられます。
- モデルクラス:
app/Models/Post.php - ポリシークラス:
app/Policies/PostPolicy.php
独自のディレクトリ構造を採用している場合など、手動でマッピングしたい場合は AppServiceProvider の boot() 内で Gate::policy() を使用して登録できます:
// app/Providers/AppServiceProvider.php
use App\Models\Post;
use App\Policies\CustomPostPolicy;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
// 手動マッピング(通常は不要)
Gate::policy(Post::class, CustomPostPolicy::class);
}
4-4. ゲスト(未ログイン)ユーザーの認可対応
デフォルトでは、PolicyやGateのメソッドは未ログイン(ゲスト)ユーザーの場合に自動的に false を返し、メソッド自体が実行されません。
未ログインユーザーにも閲覧権限(view や viewAny など)を判定させたい場合は、User 引数に ?(Null許容型)を付与するか、デフォルト値 null を指定します。
// app/Policies/PostPolicy.php
/**
* ゲストユーザー(未ログイン)も許可する閲覧判定
*/
public function view(?User $user, Post $post): bool
{
// 公開記事なら未ログインでも誰でも閲覧可能
if ($post->is_published) {
return true;
}
// 非公開記事は、ログインしていてかつ作成者の場合のみ許可
return $user !== null && $user->id === $post->user_id;
}
5. 認可の判定場所・実践的な5つの実装パターン
Laravelの優れた点は、同じPolicyやGateの認可ロジックを、コントローラ、Bladeテンプレート、ルーティング、FormRequestなどあらゆる場所から一貫した構文で呼び出せることです。
パターン①:Controllerでの判定($this->authorize / authorizeResource)
コントローラでは、$this->authorize() または Gate::authorize() を使用します。認可に失敗すると自動で 403 Forbidden 例外がスローされます。
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
public function update(Request $request, Post $post)
{
// 1. コントローラの authorize メソッドを使用(Policyのupdateメソッドを呼び出す)
$this->authorize('update', $post);
// または Gate ファサードを使用
// Gate::authorize('update', $post);
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}
}
リソースコントローラのコンストラクタで
$this->authorizeResource(Post::class, 'post'); を呼ぶだけで、index, show, create, store, edit, update, destroy の全アクションに自動で対応するPolicy認可が適用されます!
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
パターン②:Bladeテンプレートでの判定(@can / @cannot)
Bladeビューでは、ユーザーの権限に応じてボタンやリンクの表示・非表示を簡単に切り替えられます。
<!-- 記事の更新権限がある場合のみ「編集ボタン」を表示 -->
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}" class="btn btn-primary">記事を編集</a>
@endcan
<!-- 記事の削除権限がある場合のみ「削除ボタン」を表示 -->
@can('delete', $post)
<form action="{{ route('posts.destroy', $post) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">削除</button>
</form>
@endcan
<!-- 権限がない場合にメッセージを表示 -->
@cannot('update', $post)
<p class="text-muted">※この記事を編集する権限はありません。</p>
@endcannot
<!-- モデルインスタンス不要のGate判定 -->
@can('access-admin-panel')
<li><a href="/admin">管理画面トップ</a></li>
@elsecan('access-moderator-panel')
<li><a href="/moderator">モデレーター画面</a></li>
@endcan
パターン③:Route・ミドルウェアでの判定(can:ability,model)
ルーティングの段階でアクセスを遮断したい場合は、Laravel標準の can ミドルウェアを使用します。
// routes/web.php
use App\Http\Controllers\PostController;
use App\Http\Controllers\AdminController;
// 1. Policyとルートモデルバインディングを連携させた認可
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
// 2. モデル不要のGateに対するミドルウェア適用
Route::get('/admin/dashboard', [AdminController::class, 'index'])
->middleware('can:access-admin-panel');
パターン④:FormRequestでの判定(authorizeメソッド)
リクエストバリデーションクラス(FormRequest)の authorize() メソッド内でPolicyを呼び出すことで、コントローラに到達する前のバリデーション層で権限チェックを完結できます。
// app/Http/Requests/UpdatePostRequest.php
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostRequest extends FormRequest
{
public function authorize(): bool
{
// ルートパラメータからPostモデルを取得
$post = $this->route('post');
// ユーザーが更新権限を持っているかチェック
return $this->user()->can('update', $post);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
];
}
}
パターン⑤:Userモデルからの直接判定($user->can / $user->cannot)
App\Models\User モデルは標準で Authorizable トレイトを継承しているため、任意のPHPコード内でユーザーオブジェクトから直接認可メソッドを呼び出せます。
use App\Models\User;
use App\Models\Post;
$user = auth()->user();
$post = Post::find(1);
// $user->can() で判定
if ($user->can('update', $post)) {
// 編集可能
}
// $user->cannot() で判定
if ($user->cannot('delete', $post)) {
// 削除不可
}
6. 現場でよくある失敗・アンチパターンと解決策
① Controllerに if ($user->role === 'admin') をベタ書きする
コントローラのアクションごとに
if ($user->id !== $post->user_id && $user->role !== 'admin') { abort(403); } のような条件分岐を散らばらせてしまう。
✅ 解決策: 認可ロジックは必ず Policy や Gate にカプセル化し、コントローラ側は $this->authorize('update', $post) の1行に留めます。仕様変更(例:「副管理人も編集可能にする」など)があった場合もPolicyの1箇所を修正するだけで全画面に反映されます。
② Policyの自動検出が動かない・認識されない
「Policyを作ったのに This action is unauthorized. になってしまう」という場合、以下の3点を確認してください:
- モデルとPolicyの命名規則:
app/Models/Item.phpに対しapp/Policies/ItemPolicy.phpになっているか(大文字・小文字、単数形・複数形の不一致に注意)。 - メソッドの引数の型宣言: Policyメソッドの引数型(
User $user, Post $post)が実際のモデルクラスと一致しているか。 - キャッシュのクリア:
php artisan optimize:clearを実行して設定やルートのキャッシュをクリアする。
③ 403 Forbidden と 404 Not Found の使い分け(セキュリティ)
他人の非公開データや社内管理データに対して、権限がないユーザーがアクセスした際に 403 Forbidden を返すと、「そのIDのリソースが実在する」という情報が攻撃者に漏洩してしまいます。
セキュリティ要件が厳しいシステムでは、Policyやコントローラで存在自体を隠蔽するために abort(404) を返す設計も検討しましょう。
7. まとめ|GateとPolicyを使い分けて安全なLaravelアプリを構築しよう
Laravelの認可システム(Gate & Policy)の要点を振り返りましょう。
- 特定のEloquentモデル(Post, Comment, Order等)の操作 ➔ Policy(
php artisan make:policy)を作成する - モデルに依存しない汎用的な画面アクセスや機能フラグ ➔ Gate(
AppServiceProvider)を定義する - スーパー管理者(全権限付与) ➔
Gate::before()で一括バイパスを設定する - Controller・Blade・Middleware・FormRequest で統一された構文(
can,authorize)を使い、ロジックを分散させない
Laravelの認証パッケージ(Breeze / Sanctum)とGate/Policyを適切に組み合わせることで、堅牢でメンテナンス性の高いセキュアなWebアプリケーションを素早く構築することができます。ぜひ日々の開発で活用してみてください!

コメント