LaravelのGate(ゲート)とPolicy(ポリシー)は名前も名前から想像するイメージも全く異なるため別の機能だと認識していまいそうですがどちらもAuthorization(認可)に関する機能です。

認可はLaravelのドキュメントでは、”authorize user actions against a given resource”と説明されています。日本語では”あたえられたリソースに対するユーザのアクションに許可を与える”という意味になるかと思います。簡単に言えばだれにある特定の処理を行う許可を与えるかどうかです。

認可という言葉を使う機会がないのでイメージしづらいかもしれませんのでブログの例を使って説明します。あるユーザがブログの記事を作成しました。そのブログに対して誰に削除許可を与えるのかといった制限を行うことを認可といいます。許可を与えるのはそのユーザ自身のみなのかそれとも管理者のみなのかといったようにアプリケーションの要件に合わせて認可を設定していきます。

Gate(ゲート)、Policy(ポリシー)や認可(Authorization)という言葉が使われていますが要はアクセス制限です。アクセス制限ということに注目して読み進めてください。

GateとPolicyの違い

どちらも認可を与える機能ですが、Policyはある特定のモデルに対して行うアクション(作成、更新、削除、閲覧等)に関してアクセス制限を行います。そのためモデルごとに個別の独立したPolicyファイルを作成します。Gateは”主に”特定のモデルに関連していないユーザのアクションに関してアクセス制限を行う時に使用します。例えば管理者画面にアクセスできるユーザを制限させるといったことにGateを利用することができます。

Gateもモデルに対するアクションにアクセス制限を行うことは可能です。Policy、Gateのどちらか一方を使うのではなくどちらも利用しながらアクセス制限を行います。
fukidashi

動作確認のための環境準備

実際にLaravelで構築したシステムを利用した方がわかりやすいので、UserモデルとPostモデルを作成して動作確認用の環境構築を行います。

UserテーブルとPostテーブルの作成

usersテーブルとpostsテーブルの構成情報の説明を行います。

usersテーブルの作成を行うmigrationsファイルはデフォルトで作成済みです。作成済みのmigrationsファイルにroleの列を追加しています。role列では作成するユーザの役割を設定します。管理者であればadministratorのroleを設定します。


public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->string('role');
        $table->rememberToken();
        $table->timestamps();
    });
}

postsテーブルについては、ブログの記事を保存することを想定しているので、作成した作成者のuser_idと記事のタイトル(title)、内容(content)の3つの列を追加します。


public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->integer('user_id')->unassign();
        $table->string('title');
        $table->text('content');
        $table->timestamps();
    });
}

データの登録

作成したusesテーブルには5名のユーザを登録しています。

ユーザ一覧
ユーザ一覧

postsテーブルには4つの記事を登録しています。各記事にuser_idを設定しているので作成者の名前を表示させることができます。

記事一覧
記事一覧

アクセス制限とは

Gate, Policyを使用しなくてもアクセス制限を行うことはできます。アクセス制限とはどういうものかを確認するためにGate, Policyを使用せずに行なってみましょう。

記事の削除に対する制限

あるユーザがブログの記事を書いた場合そのユーザのみ削除する許可する権限を与えるとします。アクセス制限を与えるためには、2つの方法が考えられます。

  1. 削除許可がないユーザにはブラウザ上で削除ボタンを表示させない
  2. 削除処理を行わせない

実際にコードで確認していきます。

削除ボタンを非表示にする

削除許可がないユーザには削除ボタンを非表示にします。記事一覧にアクセスしているユーザのidと$postのuser_idを利用し、Bladeファイル内でif文の分岐を行えば削除ボタンの表示・非表示を制御することができます。

ユーザのidと$postのuser_idが一致する場合はボタンが表示され、一致しない場合はボタンは表示されません。


<td>
	@if(Auth::user()->id === $post->user_id)
	<form method="POST" action="/posts/{{ $post->id }}">
	@method('delete')
	@csrf
	<button type="submit" class="btn btn-danger">削除</button>
	</form>
	@endif
</td>

鈴木さんがアクセスした場合は鈴木さんが作成したID3の記事のみ削除ボタンが表示されます。

作成者以外は削除ボタンは表示されない
作成者以外は削除ボタンは表示されない

削除処理を行わせない

削除許可がないユーザに削除処理を行わなせないことで制限を行います。この場合はBladeファイルではなくコントローラー側で設定を行い、ユーザのidと$postのuser_idが一致した時のみ削除プログラムが実行されます。


public function destroy(Post $post){

        if(\Auth::user()->id == $post->user_id){

        	$post->delete();

        }else{
          // 削除する許可がない場合はなにか別の処理を行う

        }

    	return redirect('/posts');

    }

2つの例を通して、アクセス制限がどういうものか理解してもらえたのではないでしょうか。

Gate(ゲート)を利用したアクセス制限

Gateを使用してあるページへのアクセス制限を行います。ここではroleにadministratorのみをもつユーザにだけユーザ一覧のページを閲覧する権限を与えます。

GateはApp\Providersフォルダの下にあるサービスプロバイダーファイルのAuthServiceProvider.phpの中にアクセス制限を行うための定義を記述します。

定義を行う時はbootメソッドの中にGateファサードのdefineメソッドを使って行います。defineメソッドの第一非キスにはアクセス制限を行う際に利用する任意の名前、第二引数にはクロージャ―を設定します。クロージャ―の処理ではtrueかfalseかを戻します。今回の場合はroleがadministratorであるかどうかを判別する処理を記述し、roleがadministratorであればtrueを戻し、そうでなければfalseを戻します。


namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
protected $policies = [
    'App\Post' => 'App\Policies\PostPolicy',
];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();


        Gate::define('isAdmin',function($user){

           return $user->role == 'administrator';
    
        });
    }
}

ここでは定義を行なっているだけなので、実際にアクセス制限を行う場合は、制限を行いたい箇所に追加でコードを記述する必要があります。

Bladeファイルで行う制御

Gateの設定後にbladeファイルでアクセス制限を行う場合は、@canを利用することができます。


@can('isAdmin')
  // ユーザ一覧を表示するhtmlを記述
@else
<p>管理者のみユーザ一覧が表示されます。</p>
@endcan

administratorのroleを持っているユーザにはユーザ一覧が表示されます。

ユーザ一覧
ユーザ一覧

administrorのroleを持っていないユーザにはユーザ一覧は表示されません。

アクセス制限のないユーザがアクセスした場合
アクセス制限のないユーザがアクセスした場合

@canだけではなく@cannot, @cantを使用することもできます。


@cannot('isAdmin')
  // ユーザ一覧を表示するhtmlを記述

@else
<p>管理者のみユーザ一覧が表示されます。</p>
@endcan

cannotはcanの否定なるので、administratorのroleを持っていないユーザのみにユーザ一覧が表示されます。

コントローラーで行うアクセスの制御

bladeファイルではなくコントローラーの中でアクセス制限を行いたい場合は、authorizeメソッド、allowsメソッド、deniesメソッド等を利用することができます。authorizeメソッドは下記のように記述することができます。


namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\User;

use Gate;

class UserController extends Controller
{

	public function index(){

	    Gate::authorize('isAdmin');

	    $users = User::all();

	    return view('users.index',compact('users'));
	    
	}

}

authorizeメソッドを使用した場合は、roleがadministratorでない場合は403のAuthorizationExceptionがthrowされます。

authorizeメソッドは次の処理の入り口のような役割をしていると考えばようやくGate(ゲート)という名前をつけていることも理解できるのではないでしょうか。
fukidashi
403 unauthorized
403 unauthorized

allowsの場合は、AuthorizationExceptionがthrowされないので、if分により分岐処理を行うことができます。


public function index(){

    if(Gate::allows('isAdmin')){

		$users = User::all();

	}else{

		dd('ユーザ一覧にアクセスが許可されていないユーザです。');

	}

	return view('users.index',compact('users'));
    
}

deniesはallowsと逆の処理を行うことができます。deniesは設定した場合はadministratorのroleを持たないユーザがユーザ一覧を閲覧することができます。

forUserメソッドの使用方法

削除のアクセス制限を行うために他のユーザのアクセス権を利用する場合にはforUserメソッドを使用することができます。

アクセスするユーザではなくforUserメソッドに他のユーザを指定し、指定したユーザの情報で削除のアクセス制限を行うことができます。


public function destroy(Post $post){

    $other_user = User::find(1); //アクセスしたユーザではなくid1を持つユーザ利用

    if(Gate::forUser($other_user)->allows('delete-post',$post)){

        $post->delete();

    }

	return redirect('/posts');

}

Policy(ポリシー)を用いたアクセス制限

policy(ポリシー)の作成

Gateの説明では特定のモデルに関連しない処理に対してアクセス制限を行いました。Policyでは特定のモデルに関連する処理に対してアクセス制限を行なっていきます。

Policyファイルはphp aritsan make:policyコマンドで作成することができます。実行するとapp¥policiesフォルダの下にPostPolicy.phpファイルが作成されます。


$ php artisan make:policy PostPolicy
Policy created successfully.

作成されるファイルの中身は下記の通りでメソッドは何も記述されていません。


namespace App\Policies;

use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }
}

–modelオプションをつけてPolicyを作成をするとPolicyで使用するメソッドが記述された状態でファイルを作成することができます。

コントローラーを作成する時にCRUDのメソッドを記述した状態で作成する–resourceオプションに似た動作です。コントローラーでは、index, create, store, show, edit, updateメソッドが記述されます。
fukidashi

 $ php artisan make:policy PostPolicy --model=Post
Policy created successfully.

記述されているメソッドを元にPostモデルに特化したPolicyを作成することができます。PostPolicy.phpファイルには、viewAny, view, create, update, delete, restore, forceDeleteの7つのメソッドが記述された形で作成されます。


namespace App\Policies;

use App\User;
use App\Post;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;
    
    /**
     * Determine whether the user can view any posts.
     *
     * @param  \App\User  $user
     * @return mixed
     */
    public function viewAny(User $user)
    {
        //
    }

    /**
     * Determine whether the user can view the post.
     *
     * @param  \App\User  $user
     * @param  \App\Post  $post
     * @return mixed
     */
    public function view(User $user, Post $post)
    {
        //
    }

Policyを利用したアクセス制限の準備

これから削除と閲覧に対してアクセス制限を行うため、PostPolicy.phpファイルの更新を行います。

使用しないメソッドはPostPolicy.phpファイルから削除することができます。ここではこの後利用するdeleteメソッドとviewメソッドのみを残して残りのメソッドは削除しています。どちらもメソッドもアクセスしているユーザidと記事を作成したユーザidが一致するかどうかのシンプルなコードです。このファイルに記述されているメソッド名しか利用できないわけではなく任意の名前のメソッド名を追加することができます。



namespace App\Policies;

use App\User;
use App\Post;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;
 
    /**
     * Determine whether the user can view the post.
     *
     * @param  \App\User  $user
     * @param  \App\Post  $post
     * @return mixed
     */
    public function view(User $user, Post $post)
    {
       
        return $user->id == $post->user_id;
  
    }
   
    /**
     * Determine whether the user can delete the post.
     *
     * @param  \App\User  $user
     * @param  \App\Post  $post
     * @return mixed
     */
    public function delete(User $user, Post $post)
    {
        return $user->id == $post->user_id;
    }

}

作成したPolicyの情報はAuthServiceProvider.phpに登録を行う必要があります。登録を行うことでPostモデルとPostPolicyが紐づけられます。

Laravelにautodiscoveryという機能があるため$policiesに手動で設定しなくてもPolicyを自動で見つけてきてくれます。
fukidashi

protected $policies = [
    'App\Post' => 'App\Policies\PostPolicy',
];

AuthServiceProvide.phprへの追加が完了したので、Policyを使ってアクセス制限を行います。いくつか異なる方法で設定を行うことができ、下記では3つの方法を紹介しています。

authorizeメソッドを利用した制限

authorizeメソッドを記述することでPolicyでアクセス制限を行うことができます。削除に関する制限なので、コントローラーのdestroyメソッドの中に記述しています。


public function destroy(Post $post){

    $this->authorize('delete', $post);

    $post->delete();

   return redirect('/posts');

}

authorizeの最初に引数の’delete’がPostPolicy.phpファイル内に記述したdeleteメソッドを対応します。2番目の引数には、Postモデルの変数$postを指定しています。

PostPolicy.phpの中で設定したdeleteメソッドの結果、アクセスしたユーザのidと記事を作成したユーザidが一致すればそのまま処理を行います。

しかし、ユーザidと記事の作成者のidが一致しない場合は下記のようにブラウザに403 unauthorizedが表示されます。

403 unauthorized
403 unauthorized

canメソッドを利用した制限

authorizeメソッドではなくcanメソッドも利用することができます。canメソッドを利用する場合は、$userを使ってcanメソッドを使用するためauth()ヘルパー関数等を利用してユーザ情報を取得しておく必要があります。


public function show(Post $post){

    $user = auth()->user(); //アクセスしているユーザ情報を取得

    if($user->can('view',$post)){

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

    }else{

        dd('閲覧する許可がありません。');
    }

}

canメソッドではユーザのidと記事を作成したユーザのidが一致しない場合は、403 unauthorizedのExceptionがthrowされません。

canメソッドではなく逆の処理を行うcantメソッド、cannotメソッドがあります。

middleware(ミドルウェア)を利用した制限

middleware(ミドルウェア)を利用してアクセス制限を行うこともできます。下記ではweb.phpファイルのルーティングでmiddlewareの設定を行なっています。


Route::get('/posts/{post}','PostController@show')->middleware('can:view,post');

ユーザのidと記事を作成したユーザのidが一致しない場合は、403 unauthorizedのExceptionがthrowされます。

beforeメソッドについて

postPolicyの中ではアクセスしているユーザidと記事を作成したユーザidが一致する場合のみアクセスを行うことができました。ここで管理者であればすべてのリソースにアクセスできる設定を行いたい場合にbeforeを利用することができます。roleがadministratorのユーザであれば制限していたリソースへのアクセスが可能となります。


public function before(User $user){
  if($user->role === 'administrator'){
    return true;
  }
// return $user->role === 'administrator'ではない
}

PostPolicy以外にもPolicyが存在し、すべてのPolicyのアクセスについてadministraotrの許可を与える場合はPolicy毎にbeforeメソッドを設定するのではなくAuthServiceProvier.phpファイルにbeforeメソッドを設定することでadministarotrのアクセスが可能となります。

<

boot(){
  $this->registerPolicies();
  Gate::before(User $user){
    if($user->role === 'administrator'){
      return true;
    }
  }
}

Policyのメソッドについて

–modelオプションを利用してPolicyを作成するとview, updateなどのメソッドが記述された形でPolicyファイルが作成されました。

これらのメソッドは、コントローラーのメソッドと関連を持っています。

PolicyとContollerメソッドの関係
PolicyとContollerメソッドの関係

コントローラーのshowメソッドは、Policyのviewメソッドと関連を持っているので、下記のようにコントロラーのshowメソッドの中でauthorizeメソッドを実行すると自動でPostPolicy.phpファイルのviewメソッドが実行されます。


public function show(Post $post){

    $this->authorize($post);

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

}

showメソッドの中でauthorizeメソッドを実行する際に引数にviewを入れなくても自動でviewメソッドが呼ばれます。

コントローラーの__constructメソッドにauthorizeResourceメソッドを追加するとコントローラーの個別のメソッドでauthorizeメソッドを使わなくてもPolicyファイルで指定したメソッドが有効になります。


class PostController extends Controller
{

	public function __construct(){
        $this->authorizeResource(Post::class);
	}

authorizeResourceメソッドを設定することでshowメソッドの中で個別にauthorizeメソッドを使っている場合は、その行を削除してもshowメソッドのアクセス制御を行うことができます。

アクセス制限の説明を行う際にGateやPolicyを使わない方法を紹介しました。その方法に比較して、GateやPolicyを使用すると効率よく設定が行えることが理解できたかと思います。これらの機能を使わずアクセス制限をしているのであればぜひ使用してみてください。