1. はじめに
Laravelで権限管理を実装するとき、Policy と Gate のどちらを使うべきか迷ってしまいました。この記事では、ToDoアプリの実装を通じて、学習過程で理解した Policy と Gate の違いをまとめています。
認証と認可の違い
まず、基本概念を整理しておきます。
| 概念 | 説明 | 例 |
|---|---|---|
| 認証(Authentication) | ユーザーが本人か確認 | ログイン処理 |
| 認可(Authorization) | ユーザーが操作できるか確認 | 権限チェック |
認証:「あなたは誰ですか?」
認可:「あなたは何ができますか?」
Laravelでは:
認証 → ミドルウェア 'auth'
認可 → Gate または Policy
2. Gate とは
Gateは、User単位の権限チェック。
シンプルな権限判定に使うものだと理解しました。
実装例
▼app/Providers/AppServiceProvider.php
phpuse Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('is-admin', function ($user) {
return $user->role === 'admin';
});
Gate::define('delete-post', function ($user) {
return $user->role === 'admin' || $user->role === 'moderator';
});
}
Controller での使用方法
public function destroyPost(Post $post)
{
if (Gate::deny('delete-post')) {
abort(403);
}
$post->delete();
}
Viewでの使用
@can('is-admin')
<a href="/admin">管理画面</a>
@endcan
Gate が使われるケース
- 「このユーザーは管理者か?」
- 「このユーザーは投稿できるか?」
- ユーザーの属性 に基づく判定
Gate の制限事項
Gate::define('is-admin', function ($user) {
// 引数は $user のみ
// 特定のモデルオブジェクト(Post, Todo など)を引数に取れない
});
Gate は User のみを引数に取るので、特定のモデルオブジェクトを基準に判定できません。
3. Policy とは
Policy は、モデルオブジェクト単位の権限チェックです。
特定のモデルに対する権限判定に使います。
実装例
以下を実行
artisan make:policy TodoPolicy --model=Todo
▼app/Policies/TodoPolicy.php
<?php
namespace App\Policies;
use App\Models\Todo;
use App\Models\User;
class TodoPolicy
{
// 編集権限のチェック
public function update(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
// 削除権限のチェック
public function delete(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
// 閲覧権限のチェック
public function view(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
}
Controller での使用
public function edit(Todo $todo)
{
// このToDoを編集する権限があるか確認
$this->authorize('update', $todo);
return view('todos.edit', compact('todo'));
}
public function update(Request $request, Todo $todo)
{
$this->authorize('update', $todo);
$todo->update($request->validated());
return redirect()->route('todos.index');
}
ビューでの使用
@can('update', $todo)
<a href="{{ route('todos.edit', $todo) }}">編集</a>
@endcan
@can('delete', $todo)
<form action="{{ route('todos.destroy', $todo) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">削除</button>
</form>
@endcan
Policy が使われるケース
- 「このユーザーは、このTodo を編集できるか?
- 「このユーザーは、このPost を削除できるか?」
- 特定のモデルオブジェクトに基づく判定
Policy のメリット
public function update(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id; // ← Todo オブジェクトを参照できる
}
Policy の最大の強みは、引数にUserとモデルオブジェクト両方を取ることで、『特定のToDoオブジェクト』に対する権限判定ができる点です。
4. Policy 対 Gate 比較表
| 項目 | Policy | Gate |
|---|---|---|
| 用途 | モデル権限 | ユーザー権限 |
| 引数 | User + モデル | User のみ |
| 判定内容 | 「このToDoを編集できるか?」 | 「管理者か?」 |
| 複雑性 | 複雑な判定向け | シンプルな判定向け |
| ビューでの使用 | @can('update', $todo) |
@can('is-admin') |
| 例 | ToDoの所有者チェック | 管理者フラグチェック |
5. ToDoアプリでの実装例
今回ToDoアプリの作成において、なぜPolicyを選んだのか
▼ToDoアプリの要件
- 各ユーザーが自分のToDoだけ編集・削除できる
- 他のユーザーのToDoには操作できない
この要件は「特定のToDoオブジェクト」を基準に判定する必要があります。
Gate では User の情報だけでは判定できません。
// ❌ Gate では不可能
Gate::define('can-edit-todo', function ($user) {
// $user の情報だけでは、どのToDoを編集する権限があるのか判定できない
});
Policy なら可能です:
// ✅ Policy で実現可能
public function update(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id; // ← 特定のToDoを参照
}
実装コード(完全版)
1. Policy の作成と実装
artisan make:policy TodoPolicy --model=Todo
****app/Policies/TodoPolicy.php:
<?php
namespace App\Policies;
use App\Models\Todo;
use App\Models\User;
class TodoPolicy
{
public function update(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
public function delete(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
public function view(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
}
2. Controller での使用
▼app/Http/Controllers/TodoController.php
public function edit(Todo $todo)
{
$this->authorize('update', $todo);
return view('todos.edit', compact('todo'));
}
public function update(Request $request, Todo $todo)
{
$this->authorize('update', $todo);
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'is_completed' => 'boolean',
]);
$todo->update($validated);
return redirect()->route('todos.index')->with('success', '更新しました');
}
public function destroy(Todo $todo)
{
$this->authorize('delete', $todo);
$todo->delete();
return redirect()->route('todos.index')->with('success', '削除しました');
}
3. ビューでの使用
resources/views/todos/index.blade.php
@foreach ($todos as $todo)
<div class="todo-item">
<h3>{{ $todo->title }}</h3>
<!-- 権限チェック付きアクション -->
@can('update', $todo)
<a href="{{ route('todos.edit', $todo) }}">編集</a>
@endcan
@can('delete', $todo)
<form action="{{ route('todos.destroy', $todo) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit">削除</button>
</form>
@endcan
</div>
@endforeach
4.動作確認
実装後、以下のように確認しました。
ユーザーAでToDoを作成
ユーザーAで編集・削除 → 成功✓
ユーザーBでそのToDoを編集しようとする → 403エラーが出る✓
Policy が正常に機能していることが確認できました。
6.防御的プログラミングについて学ぶ
以下のようなコードを書いててどっちが良いコードなんだろうと思ったので、少し調べてみました。
A
public function handle(Request $request, Closure $next): Response
{
$todo = $request->route('todo');
// 権限があれば通す(ポジティブ判定)
if ($todo && auth()->user()->id === $todo->user_id) {
return $next($request);
}
// 権限がなければエラー
abort(403);
}
B
public function handle(Request $request, Closure $next): Response
{
$todo = $request->route('todo');
// 権限がなければ明示的にエラー(ネガティブ判定)
if ($todo && auth()->user()->id !== $todo->user_id) {
abort(403, '権限がありません。');
}
// デフォルト処理(安全)
return $next($request);
}
結論としてBの方がいいらしいです。
理由として、セキュリティが重要なアプリケーションでは、エラーケース(権限がない)を先に処理することが重要らしい。
Aでは、条件を見落とすと権限がない人も通ってしまう可能性が懸念点となる。
Bの方は、
- エラーケースを明示的に処理
- デフォルトは「許可しない」
- 条件を見落としても、権限のない状態が守られる
セキュリティの鉄則として、デフォルトは「拒否」。許可する条件だけを明確に記述する。
7. まとめ
学習過程で、こういう選択基準をまとめました。
| 判定内容 | 使うべき技術 |
|---|---|
| ユーザーが管理者か | Gate |
| ユーザーが投稿できるか | Gate |
| ユーザーがこのToDoを編集できるか | Policy |
| ユーザーがこのPostを削除できるか | Policy |
- 特定のモデルに対する権限 → Policy を使う
- ユーザーの属性に基づく権限 → Gate を使う
- 防御的プログラミングを意識する - エラーケースを先に処理する
- ビューでも@canディレクティブで権限チェックをする - 複数の層で守る