1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravelの認証・認可周りを整理してみたい(GateとPolicy)

1
Last updated at Posted at 2026-06-23

はじめに

前回は「あなたは誰?」を判定する認証の裏側を見てきました。
今回は「あなたは何ができる?」を判定する認可について見ていきます!

前回記事:Laravelの認証・認可周りを整理してみたい (auth.phpとAuthファサード)

GateとPolicy

Laravelで用意されている認可処理は、主にゲート(Gate)とポリシー(Policy)の2つがあります。

名前 役割
Gate 1つの認可処理に名前を付けて、可否を判定する
Policy 特定のモデルに関する複数の認可処理を1つのクラスにまとめる

Gateというと、通行を許可するか拒否するかをその場で判断する感じです。
Policyというと「セキュリティポリシー」などで聞き馴染みがありますが、要するに「リソースごとのルールの集合」というイメージでしょうか。

なぜ認可をコントローラに直書きしないのか

そもそも、なぜGateやPolicyに切り出すのかというところですが、
こうした認可の判定をコントローラーの処理に直接書いてしまうと、権限チェックがあちこちに散らばって重複し、条件が増えるたびにクラスがどんどん肥大化していきます。

// Bad: 認可をコントローラーに直書きしている例
class PostController extends Controller
{
    public function update(Request $request, Post $post)
    {
        $user = $request->user();
        // 権限チェックをアクション内に直書きしている
        if ($user->id !== $post->user_id && $user->role !== 'admin') {
            abort(403, 'この投稿を編集する権限がありません');
        }
        $post->update($request->validated());
        return redirect()->route('posts.show', $post);
    }

    public function destroy(Request $request, Post $post)
    {
        $user = $request->user();
        // update と同じ判定をここでもまた書いている(重複)
        if ($user->id !== $post->user_id && $user->role !== 'admin') {
            abort(403, 'この投稿を削除する権限がありません');
        }
        // さらに「公開済みは編集者でないと消せない」など条件が増えていく
        if ($post->is_published && $user->role !== 'editor' && $user->role !== 'admin') {
            abort(403, '公開済みの投稿は削除できません');
        }
        $post->delete();
        return redirect()->route('posts.index');
    }
}

本来の「リクエストを受けてレスポンスを返す」処理が、認可ロジックに若干埋もれています。
これくらいだったらいいですが、規模が大きくなるとちょっと嫌になってきますね。

結果として保守性が落ちてしまうので、認可は専用の仕組みに分けて考えよう、ということですね。
この認可ロジックを、GateやPolicyに切り出してみます。

Gate に切り出す

// Good: Gate に切り出す(AppServiceProvider::boot() 内で定義)
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;

// AppServiceProvider クラス内
public function boot(): void
{
    Gate::define('update-post', function (User $user, Post $post) {
        // 所有者本人、または管理者なら許可する
        return $user->id === $post->user_id || $user->role === 'admin';
    });
}

これで update-post という名前で認可ルールが登録され、アプリのどこからでも Gate::allows('update-post', $post) のように呼び出せるようになります。

Policy に切り出す

// Good: Policy に切り出す(1つのクラスに複数の認可処理をまとめられる)
class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        // 所有者本人、または管理者なら許可する
        return $user->id === $post->user_id || $user->role === 'admin';
    }

    public function delete(User $user, Post $post): bool
    {
        // 公開済みの投稿は管理者だけが削除できる
        if ($post->is_published) {
            return $user->role === 'admin';
        }
        // 非公開(下書き)の投稿は本人か管理者なら削除できる
        return $user->id === $post->user_id || $user->role === 'admin';
    }
}

Policyでは、update や delete のように操作ごとにメソッドを分けて、関連する認可処理を1つのクラスにまとめられます。
さらに、それぞれのメソッドの中では、早期returnや条件分岐を使って細かいルールを表現できます。たとえば上の delete では「公開済みなら管理者だけ、非公開なら本人か管理者」というように、状況に応じた判定をそのまま書けます。

コントローラはどう変わるか

コントローラー側は、認可をGateやPolicyに任せることでぐっとスッキリします。

// Good: 認可は外に切り出し、コントローラーは本来の処理に専念する
class PostController extends Controller
{
    public function update(Request $request, Post $post)
    {
        // Gate 'update-post' で認可をチェックする(ルールの中身は Gate 側)
        $this->authorize('update-post', $post);
        $post->update($request->validated());
        return redirect()->route('posts.show', $post);
    }

    public function destroy(Request $request, Post $post)
    {
        // Policy の delete メソッドで認可をチェックする
        $this->authorize('delete', $post);
        $post->delete();
        return redirect()->route('posts.index');
    }
}

特定モデルに紐付かない横断的な判定なら「Gate」、モデルにまつわる操作が増えていく(まとめておきたい)なら「Policy」と使い分けると整理しやすそうです。

artisan で雛形を作る

Laravelのartisanコマンドに、ポリシークラスの雛形を作成できるコマンドがあります。

php artisan make:policy PostPolicy --model=Post

--model=Post オプションを付けると、viewAny / view / create / update / delete / restore / forceDelete の7メソッドが雛形として入った app/Policies/PostPolicy.php が生成されます。あとは必要なメソッドの中身を書いていくだけです。

ポリシーの登録については、App\Models\Post に対して App\Policies\PostPolicy という命名規則で配置すれば、Laravelが自動でポリシーを検出してくれます。明示的な登録は不要です(命名規則から外れる場合は Gate::policy(Post::class, PostPolicy::class)AppServiceProvider::boot() で呼んで手動登録できます)。

事例:他人のリソースへのアクセスを 404 にする

いきなり少し毛色が違う話になりますが、実際にあった開発での事例をここで紹介します。

認可に失敗すると、Laravelはデフォルトで403(Forbidden)を返します。ただ、403は「そのリソースは存在するが、あなたには権限がない」ことを暗に伝えてしまうため、リソースの存在自体を隠したい場合は404を返したいことがあります。

実装方法はいくつかありますが、Policy 自体はシンプルな bool のままにして、「認可失敗時に投げられる例外を、アプリ全体で404に変換する」という書き方が便利です。

まず、コントローラ側では Gate::forUser(...)->authorize(...) で認可をチェックします。

class PostController extends Controller
{
    public function show(Request $request, Post $post): Response
    {
        // 他人の投稿IDで存在確認されないよう、認可失敗時は404扱い
        Gate::forUser($request->user())->authorize('view', $post);

        return view('posts.show', compact('post'));
    }
}

Gate::forUser($user)->authorize(...) は、認可に失敗すると例外を投げ、Laravel の標準処理で AccessDeniedHttpException(403)に変換されます。bootstrap/app.php でこの例外をさらに NotFoundHttpException(404)に書き換えると、アプリ全体で「認可失敗 = 404」に統一できます。

// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(/* ... */)
    ->withMiddleware(/* ... */)
    ->withExceptions(function (Exceptions $exceptions) {
        // 403(Forbidden)を404に変換し、リソースの存在を漏らさない
        $exceptions->render(function (AccessDeniedHttpException $e) {
            throw new NotFoundHttpException('Not Found');
        });
    })->create();

これで、Policy 側は普通に bool を返しているだけなのに、最終レスポンスは 403 ではなく 404 で返るようになります。
Policy の責務は「許可するかしないか」だけに保ったまま、「拒否されたら何を返すか」をアプリ全体の方針として外に切り出せるのがこのパターンの利点です。複数のコントローラに同じ方針を一気に適用できるので、リソース単位で書き分けるよりも漏れが出にくくなります。

なお、Policy のメソッド単位で振る舞いを変えたい場合は、戻り値型を bool から Response に変えて Response::denyAsNotFound() を返す方法もあります。「全体で統一したいときはグローバル変換、特定のメソッドだけ変えたいときは Response」と使い分けると整理しやすいです。

おわりに

Auth::user() の裏側にGuardやProviderがいたように、$user->can()$this->authorize() の裏側にはGateやPolicyが動いています。設定の意味さえ押さえれば、認可周りの短いコードが、裏でどんな仕組みを使っているのかがイメージしやすくなりそうです。

認証と認可の整理は今回で一区切りとなりますが、何か気づいたことがあればまたまとめてみたいと思います。

最後まで読んでくださり、ありがとうございました!

参考記事

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?