認証済みのユーザが持つ権限に応じて実行可能な操作を制限したいケースがよくあります。例えば、ブログアプリにおいて記事の編集・削除が可能なのは投稿者に限定するなど。所謂、アクセス権の制御が必要となるケースです。Laravelではそれら「認可」の実装方法としてPolicyが提供されています。
認証済みユーザに応じて表示を出し分ける
冒頭で紹介したブログアプリの例について考えてみましょう。以下のように会員が投稿した記事が一覧で表示されているページがあるとします。
現在、「ひかる」というユーザでログインしており、自身の投稿した記事については編集のアクションが認可されているの編集ボタンが表示されています。Policyを使わない場合は@if
などで以下の様に表示を出し分けるかと思います。
... @if (auth()->user()?->id == $article->author_id) <div class="buttons mt-2"> <a class="button is-small is-success" href="{{ route('article.edit', $article) }}"> 編集 </a> </div> @endif ...
しかし、大抵この様な表示の出し分けは1箇所に留まらず、あちこちで必要となるケースが多いです。そして都度この様な条件式を記述するとコードの見通しが悪くなるし、また判別式の条件に変更があった場合にそれら全てを更新するのは面倒です。更に他にも記事に関する権限(例えば閲覧、作成、削除など)をコントロールしたい、となるかもしれません。よって関連する認可処理をどこか適切な場所にまとめておきたいです。そうです、その適切な場所というのがPolicyという訳です。
Policyで実装してみよう
先の例をPolicyを使って書き換えてみましょう。実装の流れとしては3ステップです。
- Policyクラスの作成
- Policyクラスを登録
- Policyを使った判別処理を実装
Policyクラスの作成
Policyクラスの作成は以下のコマンドで実行できます。
php artisan make:policy ArticlePolicy
すると app/Policies/ArticlePolicy.php が作成されます。デフォルトでは以下の通り、何の変哲もない只のクラスです。
namespace App\Policies; use App\Models\User; class ArticlePolicy { /** * Create a new policy instance. */ public function __construct() { // } }
ここにArticleの編集や削除が可能か判別するupdateメソッドを追加します。以下のように
namespace App\Policies; use App\Models\User; use App\Models\Article; class ArticlePolicy { public function update(?User $user, Article $article): bool { return $user?->id == $article->author_id; } }
追加したupdate()
を見てみましょう、第一引数には認証済みのUserモデルインスタンスが渡されます。ユーザがログインしていないゲストユーザの場合はnullとなる為、’?’を付けてnullableとしています。
Policyクラスを登録する
作成したPolicyクラスを使う為にはAuthServiceProviderの$policies
に登録する必要があります。そうする事でLaravelがどのModelクラスにどのPolicyクラスが紐づいているのか判別できるようになります。
(修正:2024-03-24)
作成したPolicyクラスがLaravel標準の命名規則に則っている場合は自動でモデルに紐づくポリシークラスを見つけてくれるので登録する必要はありません。例えば次の条件を満たす場合は自動で解決されます。
- モデル名がArticle、かつ、ポリシークラス名がArticlePolicy
- Articleモデルがapp/Models配下にある
- ArticlePolicyがapp/Policies or app/Models/Policies配下にある
上記の条件を満たさない場合、つまり、標準の命名規則外である場合や独自のディレクトリ構造で運用している場合などはAuthServiceProviderの$policies
に手動で登録しましょう。以下の例では本来は登録せずとも自動解決されますが、説明の為に敢えて$policies
にてArticleとArticlePolicyの紐付けを行っています。
namespace App\Providers; use App\Models\Article; use App\Policies\ArticlePolicy; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { /** * The model to policy mappings for the application. * * @var array<class-string, class-string> */ protected $policies = [ Article::class => ArticlePolicy::class, // ←追加 ]; /** * Register any authentication / authorization services. */ public function boot(): void { // } }
Policyを使った判別処理を実装
作成したArticlePolicyを使って冒頭の編集ボタンの出し分け処理を実装してみましょう。前述した通り、こうした認可の判別処理というのはアプリの至る箇所で必要となり、それぞれの呼び出し元において適した実装方法が提供されています。以下にblade側、controller側、middleware側における例を掲載します
blade側で表示の出し分け
blade側では@can
ディレクティブを使用する事ができます。冒頭の@if
で表示を出し分けていた処理を@can
で書き換えると以下のようになります。
... @can('update', $article) <div class="buttons mt-2"> <a class="button is-small is-success" href="{{ route('article.edit', $article) }}"> 編集 </a> </div> @endcan ...
@can
は第一引数にPolicyクラスのメソッド名、第二引数に判別対象のモデルを指定します。するとLaravelは指定されたモデルを元にAuthServiceProviderで指定した$policies
を見てどのポリシークラスを参照すべきか判断します。そして、紐づくポリシークラス内から第一引数で指定されたメソッドを見つけて実行してくれます。因みに、@can
は@if
同様に@can...@else...@can
とする事もできます。また、@cannot
というディレクティブもあるので適宜使い分けてみてください。
controller側で判別する
続いて、controller側でPolicyを使った判別処理の書き方を紹介します。blade側で編集ボタンが非表示となっていても、悪質なユーザがURLを直接参照して編集画面を開いてしまうかもしれません。記事の編集画面のcontroller側にも判別処理を実装してそれを防ぎましょう。3つ方法があります。
- can()/cannot()
- authorize()
- Gate::allow()/Gate::denies()
1つずつみていきましょう。まずcan()
から。こちらはUserモデルのcan()
をコールして判別する方法です。もしUser以外の認証モデルで使用する場合はAuthorizableトレイトがuseされている必要があるのでご注意を。ArticleControllerのedit()
にて以下のように実装しました。
... public function edit(Article $article) { $user = auth()->user(); if ($user->cannot('update', $article)) { abort(403, '他人の記事は編集できません!'); } return $user->name.'の記事の編集ページ'; } ...
ユーザが記事をupdateできるなら「○○の記事の編集ページ」と表示され、不可なら「他人の記事は編集できません!」と表示されます。口語的なコードで分かりやすいですね。因みに上記ではcannot()
を使用していますが、cant()
としてもOKです。
次にcontrollerクラスのauthorize()
を使用する方法です。認可されない場合は403ページが返されます。こちらはcan()
の様に細かい指定ができませんが、短く書くことができます。
... public function edit(Article $article) { $this->authorize('update', $article); return auth()->user()->name.'の記事の編集ページ'; } ...
もし、can()
の時の様にエラー文言を指定したい場合はArticlePolicyのupdate()
にてResponseクラスを返却するようにします。
... public function update(?User $user, Article $article): Response { return ($user?->id == $article->author_id) ? Response::allow() : Response::deny('他の人の記事は編集できません!'); } ...
最後にGateファサードを使用する例です。実はPolicyにおけるコアな処理はGateが担っています。can()
やauthorize()
も内部的にはGateのメソッドをcallしています。その為、Gateのallows()
やdenies()
を直接呼び出して判別する事もできます。以下の様に。
... public function edit(Article $article) { if (Gate::denies('update', $article)) { abort(403, '他の人の記事は編集できません!'); } return auth()->user()->name.'の記事の編集ページ'; } ...
allows()
,denies()
以外にもGateには様々なメソッドが用意されていて応用が効きます。それらについては長くなってしまうので次回に紹介したいと思います。
middlewareで判別する
Controller側で判別する方法を紹介しましたが、middlewareで判別する方法もあります。そちらの実装はRouteを定義する際にメソッドチェーンでcan()
が使用できます。以下の様に。
... Route::get('article/{article}/edit', [ArticleController::class, 'edit']) ->can('update', 'article') ->name('article.edit'); ...
こちらもController側の判別処理で紹介したauthorize()
と同様に、不認可であれば403ページを返します。