WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

LaravelのGateとPolicyの仕組みを理解!認可を使いこなせるようになるために

こんにちは!
ホワイトプラスのコアシステム開発Gエンジニアのさとうです。

開発をしていく中で、複雑化したシステムを改善するのに認可を整理すると良いかも…という話題が上がりました。

PHP・Laravelを使って開発をしているのですが、Laravelの認可機能については「そういうものがあるらしい」程度の理解しかありません。

そこで、この記事では改めてLaravelの認可について調べてみたことをまとめました。

  • Laravelの認可の設定がよくわからない
  • ユーザーの属性を調べるif文がいろいろなところに散りばめられている

こんな悩みをお持ちの方は、ぜひご一読いただけると嬉しいです。

認可とは?

認可とは何か

認可とは、認証済みのユーザーが「何をできるか」を制御する仕組みのことです。

例えば、次のようなケースがあります。

  • SNSで、自分の投稿は削除できるけど、他人の投稿は削除できない
  • Webサービスで、ログインするとアクセスできる特定の画面がある

このように、ユーザーがアプリケーション上で「何ができるか、何ができないか」をコントロールするのが認可です。

Laravelで認可を実現する方法

Laravelで認可を実現する仕組みにはGate(ゲート)Policy(ポリシー)という2つの仕組みがあります。

公式ドキュメントでは、それぞれ次のように説明されています。

  • Gate:クロージャベースのシンプルな認可を実現する仕組み
  • Policy:特定のモデルなどに関連するロジックの認可をまとめる仕組み

これらを使い分けることで、柔軟でわかりやすい認可の実装が可能になります。

それぞれ使い方を検証してみました。

Laravelの認可機能の検証環境

  • PHP 8.2
  • Laravel 10

Laravelの認可その1:Gate

Gateからまずは見ていきましょう。
ポイントはシンプルな認可です。

Gateの基本的な使い方

Gateの登録方法

AuthServiceProviderboot() メソッドに Gate::define() メソッドを使用してGateを定義します。
Gate::define() メソッドの第1引数には必ず認証に用いているユーザーのモデルを渡す必要があります。

<?php

class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::define(<判定ルール名(アビリティ)>, <コールバック>);
    }
}

Gateの呼び出し方法

Gateファサードに用意された判定メソッドを使用して許可されているか判定します。

if (Gate::allows(<アビリティ>, <コールバックの第2引数以降に指定したもの>))

判定メソッドには以下の種類があります。

メソッド 判定内容
allows() 指定したアビリティを認可するかどうか確認し、true もしくは false を返す。
denies() 指定したアビリティを認可しない場合に true を返し、認可されている場合に false を返す。
check() 複数のアビリティを確認する場合に使用し、すべてのアビリティが認可されていれば true を返し、1つでも認可されていないものがあれば false を返す。
any() 複数のアビリティのうち、どれか1つでも認可されていれば true を返し、すべて認可されていない場合に false を返す。
none() 複数のアビリティを確認する場合に使用し、全てのアビリティが認可されていない場合に true を返し、1つでも認可されていれば false を返す。
authorize() 指定したアビリティを認可するか確認し、認可されていなければ AuthorizationException をスローする。

Gateの具体的な利用例

プロダクト管理画面を例に、具体的な実装を見ていきましょう。
複数プロダクトを運営しているとして、それぞれを社内のグループが管理しているイメージです。

まずはGateを登録します。
グループAはプロダクトAの管理画面へ、グループBはプロダクトBの管理画面へアクセスする認可を設定します。

  • AuthServiceProvider
<?php

class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::define(
            'accessToProductionA',
            static fn (User $user): bool => $user->group === 'groupA',
        );
        Gate::define(
            'accessToProductionB',
            static fn (User $user): bool => $user->group === 'groupB',
        );
    }
}

それぞれのコントローラーではアクセス可否を判定します。

  • ProductionAController
<?php

class ProductionAController extends Controller
{
    public function __invoke(Request $request)
    {
        if (! Gate::allows('accessToProductionA')) {
            abort(403);
        }
        return view('service_a');
    }
}
  • ProductionBController
<?php

class ProductionBController extends Controller
{
    public function __invoke(Request $request)
    {
        if (! Gate::allows('accessToProductionB')) {
            abort(403);
        }
        return view('service_b');
    }
}

これでグループAのユーザーはプロダクトBに、グループBのユーザーはプロダクトAにアクセスできなくなりました。

Gateのメリット

非常にシンプルで、コード量も少ないですね!
あまりにシンプルすぎてコード内にif文を書いても良いのでは...と思った方もいらっしゃるのではないでしょうか?
具体例では1か所ずつしか認可を判定している所がありませんでした。
しかし、これがもしいろいろなところに散りばめられていたら...?
「プロダクト横断で管理するグループCを作ることにしたから!」と言われた日には大変ですよね...

Gateを使っていれば、下記のように少し変更するだけで解決です。

class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::define(
            'accessToProductionA',
-            static fn (User $user): bool => $user->group === 'groupA',
+            static fn (User $user): bool => in_array($user->group, ['groupA', 'groupC'], true),
        );
        Gate::define(
            'accessToProductionB',
-            static fn (User $user): bool => $user->group === 'groupB',
+            static fn (User $user): bool => in_array($user->group, ['groupB', 'groupC'], true),
        );
    }
}

小規模なアプリケーションや、単純なロジックで十分な場合には満足できる機能です。

Laravelの認可その2:Policy

続いてPolicyを見ていきましょう。
Policyはモデルに対する認可をまとめるのに便利とのことでした。

Policyの基本的な使い方

Policyの作成

まずはPolicyクラスを作成します。

下記コマンドで作成することができます。

php artisan make:policy <Policy名>

リソースコントローラーの機能を活用している場合は、オプションでモデルを指定すると、リソースコントローラーにより処理されるアクションに紐づいたアビリティが用意されているPolicyクラスが生成されます。

php artisan make:policy <Policy名> --model=<モデル名>

メソッド(アビリティ)の定義

作成したクラスにメソッド(アビリティ)を作成します。

<?php

class <Policy名>
{
    public function <アビリティ名>(User $user): bool
    {
        // 条件
    }

    public function <アビリティ名>(User $user, <Policyを適用するモデル>): bool
    {
        // 条件
    }
}

アビリティの第1引数は必ず認証に用いているユーザーのモデルです。
Gateで例示したように、ユーザーの属性のみを用いた認可であれば、第1引数の指定のみで問題ありません。
ユーザー属性と認可を制御したいモデルの属性を比較したい場合には第2引数に対象のモデルを指定しましょう。

Policyの登録

PolicyもGateと同様にAuthServicePorviderで登録します。
Gateとは異なり、$policies クラス変数に連想配列の要素として設定します。

<?php

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        <Policyを適用するモデルの::class定数> => <Policyクラスの::class定数>,
    ];
}

Policyの呼び出し方

Gateファサードに用意された判定メソッドを使用して許可されているか判定します。
Policyというモデルと紐づけてまとめる仕組みはありつつも、中身はGateなんですよね。

if (Gate::allows(<アビリティ>, <Policyを適用するモデル>))

使用できるメソッドは「Gateの呼び出し方法」の項をご確認ください。

この第2引数の::class定数を用いて AuthServiceProvider$policies からPolicyを参照しています。
そのため、ユーザー属性のみしか判定に使用しないので、アビリティ定義の第2引数を設定していない場合、Policyを適用するモデルの::class定数を判定メソッドの第2引数に指定する必要がある点にご注意ください。
(後続の利用例で具体的に紹介します。)

Policyの具体的な利用例

ありきたりですがブログ記事を例に、具体的な実装例を見ていきます。

Policyクラスはオプションを付けずに生成します。

php artisan make:policy PostPolicy

アビリティは2つ定義します。
投稿は編集権限を持つ人、更新は編集権限を持つ投稿者しかできないようにします。

<?php

class PostPolicy
{
    public function create(User $user): bool
    {
        return $user->isEditor();
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->userId && $user->isEditor();
    }
}

続いてPolicyを登録します。
Gateで使用した boot() メソッドは特に実装する必要はありません。

<?php

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];
    
    public function boot(): void
    {
        //
    }
}

あとはアクセス可否を実装します。

<?php

class PostController extends Controller
{
    public function create(Request $request)
    {
        if (! Gate::allows('create', Post::class)) {
            abort(403);
        }
        // 省略
    }
    
    public function update(Request $request)
    {
        // 省略
        $post = $this->postRepository($postId); // Postクラスのインスタンスが返る
        if (! Gate::allows('update', $post)) {
            abort(403);
        }
        // 省略
    }
}

判定メソッドの引数について、「アビリティの第1引数のユーザーは認証情報から取ってこれるから省略していて、アビリティの第2引数以降を判定メソッドの第2引数以降で渡すんだな!」と最初理解していました。
その理解で第2引数が無いアビリティを検証したところ、

if (! Gate::allows('update')) { // 第2引数を設定していない
    abort(403);
}

と実装して、条件を満たしていても何度も403ページに飛ばされてしまいました...
「Policyの呼び出し方」の項の説明で出てきましたが、判定メソッドの第2引数はモデルとポリシーを結びつける大事な役割を持っているので、省略しないようにご注意ください。

(参考)割り込みチェック機能

Laravelの認可には「ゲートチェックの割り込み」というものがあります。
利用例では基本的な使い方として編集者権限にフォーカスしました。
「ゲートチェックの割り込み」は、すべての機能を使える管理者権限を作成したいというような場面で有効な機能です。

Gate::before() メソッドを使用することで、他の認可チェックに先立って判定を行うことができるようになります。
全認可チェックの前に判定されるということで、AuthServiceProviderに記述するのが一般的です。

使い方は以下のとおりです。

<?php

class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::before(<コールバック>);
    }
}

アビリティを指定する必要はなく、判定用のコールバックを渡すだけとなっています。

管理者権限を持つユーザーがすべての機能を使えるようにするには、次のように記載します。

<?php

class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::before(static fn (User $user) => $user->isAdmin());
    }
}

このように、Gate::before() を使用することで、特定の条件下で一貫性のある認可ルールを適用するのに役立ちます。

Policyのメリット

Policyのメリットは以下のように考えます。

  • 認可ロジックを1か所にまとめられるため、保守性が向上する
  • モデルごとに認可を整理できるため、大規模なアプリケーションで特に有効

判定メソッドがGateと同じなので、Gateで小さく始めたものを容易にPolicyに切り替えられそうなのも嬉しいですよね!

まとめ:GateとPolicyを使い分けて認可制御をシンプルに

この記事ではLaravelにおける認可の実現方法を調査しました。

個人的な理解としては、以下の認識です。

  • アプリケーション全体に関わるシンプルな認可はGate
    • ロールなどユーザーの属性を元にアプリケーション各所で使われる認可
    • 認可ロジックの種類が増えてきたらPolicyにまとめるのも良さそう
  • モデルに紐づく認証はPolicy
    • 記事や注文などモデルに複数操作がある場合の認可

Gate・Policyの2種類あることが理解するうえでハードルでした。
それぞれに特徴があり、上手く使い分けることがシンプル・柔軟な認可制御につながりそうですね。
ここではGate・Policyの基本的な使い方を検証してみましたが、ドキュメントを見るとまだまだ使い方がありそうです。
他の使い方も学びながら今後の改善に役立てていきたいです!

さいごに

ホワイトプラスでは、ビジョンバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など、「生活領域×テクノロジー」で事業を展開しています。
弊社に興味がある方は、オウンドメディア「ホワプラSTYLE」をご覧ください。オンラインでのカジュアル面談も可能ですので、ぜひお気軽にお問い合わせください。

参考