320
306

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Laravel】 認証や認可に関する補足資料

Last updated at Posted at 2019-01-22

はじめに

いつもどおり,以下のマニュアルの一通りの確認を先に。

「認証」と「認可」の違い

これを分かっていないと話にならないので念の為記載。

概念の違い

名称 説明
認証
Authentication
  • 本人であることを証明すること
  • 1人1人を識別すること
認可
Authorization
  • アクションを行うための権限を与えること
  • アクションを行う権限があるか確認すること
 

ステータスコードや例外クラス名の違い

名称 HTTP
Status
Application Exception HTTP Exception
認証
Authentication
401
Unauthorized
AuthenticationException UnauthorizedHttpException
認可
Authorization
403
Forbidden
AuthorizationException AccessDeniedHttpException

401 Unauthenticated のほうがどう考えても正しいが,そうなってないのには歴史的な理由があるのだろうか…?

認証のアーキテクチャ

  • Auth ファサードあるいはコンテナから "auth" のエイリアス名でアクセスできる,認証サービスのルートオブジェクトが AuthManager である。ここが認証サービスの中核となる。
  • Guard が認証手段ごとの実装として存在し,ロジックの多くはここに集約される。 このクラスは Guard 契約 を実装しなければならない。
  • ユーザを取得してくる部分だけが UserProvider に抽象化して分離される。このクラスは UserProvider 契約 を実装しなければならない。
  • ユーザとして用いるクラスは Authenticatable 契約 を実装しなければならない。

認証済みのユーザを取得する場合

デフォルトの認証手段である SessionGuard を利用する場合を例として示す。

session-user

新しくユーザを認証する場合

デフォルトの認証手段である SessionGuard を利用する場合を例として示す。

session-attempt.png

※ 本当は 3. validate() と書きたかったのだが,コード上何故かこのメソッドが使用されていなかったので避けた

AuthManager

ではソースコードを確認し,どんなメソッドがあるか確認してみよう。

AuthManager の概要

まず,全体像を掴むためのメソッドを列挙する。

class AuthManager implements FactoryContract
{
    /* ... */

    /**
     * Attempt to get the guard from the local cache.
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
     */
    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
    }

    /* ... */

    /**
     * Dynamically call the default driver instance.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->guard()->{$method}(...$parameters);
    }
}

以下の2点は必ず押さえておきたい。

  • AuthManager::guard() メソッドは Guard のインスタンスを返す。
    1. 引数として渡されたガード名,もしくは getDefaultDriver() メソッドでデフォルトのガード名を取得
    2. 生成済み Guard のインスタンスキャッシュがあればそれを返し,無ければ resolve() メソッドで生成してキャッシュしてから返す
  • AuthManager::__call() メソッドによって呼び出しの多くは Guard に委譲される。

Guard の解決方法

では,具体的に Guard はどのように解決されるのだろうか?

class AuthManager implements FactoryContract
{

    /* ... */

    /**
     * Resolve the given guard.
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
     *
     * @throws \InvalidArgumentException
     */
    protected function resolve($name)
    {
        $config = $this->getConfig($name);

        if (is_null($config)) {
            throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
        }

        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($name, $config);
        }

        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($name, $config);
        }

        throw new InvalidArgumentException("Auth driver [{$config['driver']}] for guard [{$name}] is not defined.");
    }

    /**
     * Create a session based authentication guard.
     *
     * @param  string  $name
     * @param  array  $config
     * @return \Illuminate\Auth\SessionGuard
     */
    public function createSessionDriver($name, $config)
    {
        /* ... */
    }

    /**
     * Create a token based authentication guard.
     *
     * @param  string  $name
     * @param  array  $config
     * @return \Illuminate\Auth\TokenGuard
     */
    public function createTokenDriver($name, $config)
    {
        /* ... */
    }

    /**
     * Get the guard configuration.
     *
     * @param  string  $name
     * @return array
     */
    protected function getConfig($name)
    {
        return $this->app['config']["auth.guards.{$name}"];
    }

    /**
     * Get the default authentication driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['auth.defaults.guard'];
    }

    /* ... */

    /**
     * Register a custom driver creator Closure.
     *
     * @param  string  $driver
     * @param  \Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback;

        return $this;
    }
}

Laravel らしい,マジックメソッドを使ったコードになっている。

  • getDefaultDriver() メソッドは "auth.defaults.guard" 設定の値を返す。
    • この値はデフォルトでは "web" になっている
  • getConfig()['driver']"auth.ガード名.driver" 設定の値を返す。
    • この値はデフォルトでは "session" になっている
  • resolve() メソッドは createXXXDriver() という名前のメソッドを検索し,あればそれを実行する。
    • デフォルトでは createSessionDriver() メソッドが呼ばれ, SessionGuard が生成される
  • resolve() メソッドは Auth::extend() メソッドの呼び出しで登録されたカスタムガード生成関数がある場合,それを実行する。

カスタムガード登録の例(公式マニュアルより)↓

class AuthServiceProvider extends ServiceProvider
{
   /**
    * サービスの初期起動後の登録実行
    *
    * @return void
    */
   public function boot()
   {
       $this->registerPolicies();

       Auth::extend('jwt', function($app, $name, array $config) {
           // Illuminate\Contracts\Auth\Guardのインスタンスを返す

           return new JwtGuard(Auth::createUserProvider($config['provider']));
       });
   }
}

UserProvider の解決方法

Guard に続いて, UserProvider の解決方法も見てみよう。大部分は CreatesUserProviders トレイトに委譲されているが,ほとんど Guard と同じロジックであるため詳細は省略する。

class AuthManager implements FactoryContract
{
    use CreatesUserProviders;

    /* ... */

    /**
     * Register a custom provider creator Closure.
     *
     * @param  string  $name
     * @param  \Closure  $callback
     * @return $this
     */
    public function provider($name, Closure $callback)
    {
        $this->customProviderCreators[$name] = $callback;

        return $this;
    }

    /* ... */
}

デフォルトでは EloquentUserProvider が選択されるようになっており, Auth::provider() でカスタマイズできることが分かる。

次章からは,実装まで見ると長くなるため,契約を中心として見ていくことにする。要するに,使い方だけ覚えられれば問題ない,という考え方だ。

Guard

Guard 契約StatefulGuard 契約 およびその一部のデフォルト実装である GuardHelpers を確認する。

以下は Guard 契約 として定義されているメソッドだ。これらのメソッドは必ずすべての Guard が実装しなければならない。重要な箇所を太字にして示す。

メソッド 説明
user セッションID・アクセストークンなどをもとに
認証済みユーザを検証し, Authenticatable または null を返す。
Authenticatable はキャッシュされる。 
validate Eメールアドレスやパスワードなどのクレデンシャル情報を検証して結果を論理値で返す。
これを実行するだけではログインしたことにならない。
check user() の返り値を論理値に変換して返す。
guest check() の返り値を否定して返す。
id user() の返り値から取り出したIDまたは null を返す。

以下は契約としては定義されていないが,ヘルパーに存在することから暗黙的に必須とされている場合があるメソッドだ。これらはフレームワークのバージョンアップで契約に追加される可能性もある。以下に一部を省略して示す。

メソッド 説明
hasUser Authenticatable がキャッシュされた状態かどうかを確認する。
これを実行しても認証処理は発生しない。
setUser 指定した Authenticatable をキャッシュする。
authenticate validate() と同等だが,検証失敗時に AuthenticationException をスローする。 

hasUser() は副作用を一切発生させずに,キャッシュが存在する場合のみ Authenticatable を取得したい場合に非常に便利だ。 Eloquent Model 定義など,例外や余計なHTTP通信が絶対に起こってはいけない場所で重宝する。例えば管理者にだけ見える属性を定義したい場合,以下のような書き方をするとよいだろう。

認証済みユーザが管理者である場合のみ super_secret_id を可視属性に加える
public function getVisible(): array
{
    return Auth::hasUser() && Auth::user()->isSuperAdmin()
        ? array_merge($this->visible, ['super_secret_id'])
        : $this->visible;
}

以下は StatefulGuard 契約 として定義されているメソッドだ。 SessionGuard などステートフルな Guard にはこれらの実装も必要とされる。

メソッド 説明
attempt validate() と同等だが,検証成功後に自動的に login() を実行する。 
login 指定した Authenticatable をキャッシュする。
リクエストを超えてキャッシュは保持される。
logout Authenticatable のキャッシュをクリアする。
viaMember 自動ログイントークンを使ってログインしているかどうかを返す。
once login() を使わずに直接 Authenticatable をキャッシュする。
リクエストを超えてキャッシュは保持されない。
loginUsingId 指定したIDで Authenticatable をフェッチして login() を実行する。

Authenticatable

Authenticatable 契約を確認しよう。これが既定の App\UserAuthenticatable トレイト を使ってデフォルトで実装されている。

メソッド 説明
getAuthIdentifierName id など,識別子の名前を返す。
getAuthIdentifier 識別子の値を返す。
getPassword パスワードハッシュの値を返す。
getRememberToken 自動ログイントークンの値を返す。
setRememberToken 自動ログイントークンをセットする。
getRememberTokenName remember_token など,自動ログイントークンの名前を返す。

厄介なのが, StatefulGuard を利用しなくても Authenticatable に自動ログイントークンの実装が強制されていることだ。認証をすべて外部サービスに委譲している場合などには余計なお節介だ。こういう場合,以下のようなトレイトを作って App\User にミックスインするといいだろう。

app/Auth/StatelessAuthenticatable.php
<?php

declare(strict_types=1);

namespace App\Auth;

use Illuminate\Database\Eloquent\Model;

/**
 * Trait StatelessAuthenticatable
 *
 * @mixin Model
 */
trait StatelessAuthenticatable
{
    /**
     * Get the name of the unique identifier for the user.
     *
     * @return string
     */
    public function getAuthIdentifierName()
    {
        return $this->getKeyName();
    }

    /**
     * Get the unique identifier for the user.
     *
     * @return mixed
     */
    public function getAuthIdentifier()
    {
        return $this->{$this->getAuthIdentifierName()};
    }

    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword()
    {
        throw new \BadMethodCallException('Unexpected method call');
    }

    /**
     * Get the token value for the "remember me" session.
     *
     * @return string
     */
    public function getRememberToken()
    {
        throw new \BadMethodCallException('Unexpected method call');
    }

    /**
     * Set the token value for the "remember me" session.
     *
     * @param string $value
     */
    public function setRememberToken($value)
    {
        throw new \BadMethodCallException('Unexpected method call');
    }

    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     */
    public function getRememberTokenName()
    {
        throw new \BadMethodCallException('Unexpected method call');
    }
}

UserProvider

続いて,UserProvider 契約を確認しよう。

メソッド 説明
retrieveById id など,識別子を使って Authenticatable を取得する。
retrieveByCredentials Eメールアドレスなど,ログインフォームへの入力情報を使って Authenticatable を取得する
validateCredentials パスワードなど, ログインフォームへの入力情報を取得した Authenticatable を使って検証する
retrieveByToken 自動ログイントークンを使って Authenticatable を取得する。
updateRememberToken 自動ログイントークンを更新する。

これも上記同様,外部で発行されたトークンを用いて認証を委譲している場合などには不必要なメソッドが複数存在する。該当する場合は,独自の UserProvider で null をリターンするか BadMethodCallException をスローすると良いだろう。

RequestGuard の利用

外部で発行されたトークンを用いて認証を委譲している場合などに対応するために,独自の Guard を作成することもできるが,ロジックがシンプルである場合には RequestGuard を利用するだけで解決するだろう。自作するかどうかは以下のように判断すると良い。

  • カスタマイズする内容は user() の実装のみで,ガード自体にメソッドを追加したりする必要がない。ロジックも大量にメソッド分割しなければならないほど複雑ではない。
    • はい → RequestGuard で十分
    • いいえ → Guard の自作を検討
UserProviderを無視する場合
Auth::viaRequest('custom-token', function (Request $request): ?User {
    return User::where('token', $request->token)->first();
});
UserProviderの仕組みに乗る場合
Auth::viaRequest('custom-token', function (Request $request, UserProvider $provider): ?User {
    return $provider->retrieveByCredentials($request->only('token'));
});

なお,ライブラリとして Guard を実装する場合にはできるだけ UserProvider の仕組みに乗っかるべきだが,アプリケーションを作るだけなら過度な抽象化に付き合う必要はなく, UserProvider を使わない Guard が存在しても特に問題はない。

Authenticate ミドルウェア

ここまで認証のための処理を見てきたが,リクエストを処理する際に認証処理を呼び出す起点になるのが Authenticate ミドルウェア である。あらかじめ定義された Guard が順番に認証を試み,成功した時点で Authenticatable を取得して処理を続行,という動きをする。以下の図のようなイメージだ。

authenticate-middleware.png

では,この実装を読んでみよう。

<?php

namespace Illuminate\Auth\Middleware;

use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;

class Authenticate
{
    /**
     * The authentication factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string[]  ...$guards
     * @return mixed
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($request, $guards);

        return $next($request);
    }

    /**
     * Determine if the user is logged in to any of the given guards.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  array  $guards
     * @return void
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    protected function authenticate($request, array $guards)
    {
        if (empty($guards)) {
            $guards = [null];
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        throw new AuthenticationException(
            'Unauthenticated.', $guards, $this->redirectTo($request)
        );
    }

    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     */
    protected function redirectTo($request)
    {
        //
    }
}
class AuthManager implements FactoryContract
{
    /* ... */

    /**
     * Set the default guard driver the factory should serve.
     *
     * @param  string  $name
     * @return void
     */
    public function shouldUse($name)
    {
        $name = $name ?: $this->getDefaultDriver();

        $this->setDefaultDriver($name);

        $this->userResolver = function ($name = null) {
            return $this->guard($name)->user();
        };
    }

    /* ... */

}

コードを読むポイントは2つ。

  • $guards には, App\Http\Kernel または App\Providers\RouteServiceProvider でルーティング定義の際に auth:token,session のような形式でミドルウェアを利用した場合, ガード名の配列が入ってくる。
    • 先頭の Guard から順番にチェックされ,認証が成功した時点で終了する。
    • 最後まで認証できなかった場合には AuthenticationException をスローする。
  • shouldUse() メソッドでは,ユーザ取得に成功した場合のみ $this->userResolver を上書きしている。

コントローラメソッドにて,ファサード形式で

public function something(Request $request)
{
    $user = Auth::user();
    /* ... */
}

なんて使い方をしたりするが,上記ゆえに実は引数の Request をそのまま流用して

public function something(Request $request)
{
    $user = $request->user();
    /* ... */
}

なんて形でも取れたりする。
では,認証はこのぐらいにしておいて,認可に進もう。

認可のアーキテクチャ

認証したユーザに対し,具体的にどんな動作を許可できるかどうかを定義するのがこのレイヤーだ。

authorization.png

Gate が認可サービスのルートオブジェクトになり,一連の処理の中核となる。同名のファサードに対応するのもこのクラスだ。そして,以下のような処理をすべて担う。

  • 単一の権限判定関数として, Ability を Gate::define() で登録。
  • Eloquent Model に対する複数の権限判定メソッドの集合であるクラスとして, Policy を Gate::policy() で登録。
  • Gate::authorize() で権限名称から使用すべき関数・メソッドを特定して,認可処理を行い,失敗した場合には AuthorizationException をスローする。

Gate の概要

Gate を自前で実装することはほぼ無いが,一応 Gate 契約 は存在する。使い方を確認するにはちょうどいいので,まずこれを確認しよう。

登録用メソッド

メソッド 説明
define コールバックを Ability として登録する。
policy クラスを Policy として登録する。
before 認可処理の前処理を登録する。
null 以外の値を返すと認可処理をスキップできる。
after 認可処理の後処理を登録する。

App\Providers\AuthServiceProviderregisterPolicies() メソッドで自動的に Gate::policy() が呼ばれるため,明示的にこれを呼ぶことはあまり無いと思われる。前処理・後処理も特段込み入ったことをしない限りは必要ない。

判定用メソッド

メソッド 説明
raw before → コールバック → after の順に実行する。
authorize raw() の返り値が
allows raw() を論理値に変換して返す。
AuthorizationException 安全
denies raw() の否定を論理値にして返す。
AuthorizationException 安全
check 複数の raw() の返り値がすべて truthy かどうかを返す。
AuthorizationException 安全
any 複数の raw() の返り値のいずれか1つ以上が truthy かどうかを返す。
AuthorizationException 安全

ここで「AuthorizationException 安全」と書いたものは, AuthorizationException が発生したとき,キャッチして false をリターンする。

基本的に最も頻繁に使用されるのは Gate::authorize() で,これは後述する Authorize ミドルウェア から呼ばれることになる。そのほか,局所的に Gate::allows() Gate::denies() などの出番もありそうだ。

その他のメソッド

メソッド 説明
forUser 認証ユーザを切り替える。
getPolicyFor Eloquent Model クラス名に対応する Policy クラスのインスタンスを返す。
has 指定した Ability が登録されているかを確認する。
Policy として登録されたものには反応しない。
abilities Ability 一覧を返す。
Policy として登録されたものには反応しない。

こちらも,バッチ処理などで局所的に Gate::forUser() を使うことはあるだろう。このメソッドは**新しい Gate インスタンスを返す**ため,グローバルを汚さない。

/**
 * Get a gate instance for the given user.
 *
 * @param  \Illuminate\Contracts\Auth\Authenticatable|mixed  $user
 * @return static
 */
public function forUser($user)
{
    $callback = function () use ($user) {
        return $user;
    };

    return new static(
        $this->container, $callback, $this->abilities,
        $this->policies, $this->beforeCallbacks, $this->afterCallbacks
    );
}

認可メソッド実装のルール

Response クラスとは?

さて,ここで唐突に Response というクラスが出てきた。

<?php

namespace Illuminate\Auth\Access;

class Response
{
    /**
     * The response message.
     *
     * @var string|null
     */
    protected $message;

    /**
     * Create a new response.
     *
     * @param  string|null  $message
     * @return void
     */
    public function __construct($message = null)
    {
        $this->message = $message;
    }

    /**
     * Get the response message.
     *
     * @return string|null
     */
    public function message()
    {
        return $this->message;
    }

    /**
     * Get the string representation of the message.
     *
     * @return string
     */
    public function __toString()
    {
        return (string) $this->message();
    }
}

マニュアルに一切登場しないが,どうやらこのクラスは認可成功時にもメッセージを返せるようにするためのものらしい。あまり使う機会は無さそうだが,もし「認可された理由」をユーザに返却したい場合には採用してみるとよいだろう。

論理値を返すべきか,それとも例外を投げてもいいか

マニュアルの例を見ると,論理値を true false で返しているだけのものが目立つ。

Gate::define('update-post', function ($user, $post) {
   return $user->id == $post->user_id;
});
<?php

namespace App\Policies;

use App\User;
use App\Post;

class PostPolicy
{
   /**
    * ユーザーにより指定されたポストが更新可能か決める
    *
    * @param  \App\User  $user
    * @param  \App\Post  $post
    * @return bool
    */
   public function update(User $user, Post $post)
   {
       return $user->id === $post->user_id;
   }
}

著者自身も当初はこの実装が最も正しく美しいと考えていたが,

ということを踏まえると,極端な話こんな実装もアリといえばアリだろうか。

<?php

namespace App\Policies;

use App\User;
use App\Post;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Access\Response;

class PostPolicy
{
   public function update(User $user, Post $post)
   {
       if ($user->isSuperAdmin()) {
           return new Response('本人じゃないけど管理者だから特別にOK');
       }
       if ($user->id !== $post->user_id) {
           throw new AuthorizationException('本人しか更新はできません');
       }
       return new Response('本人だから更新してOK');
   }
}

現実的に考えると,落とし所としてはこれぐらいが妥当であろう。

  • 基本的には論理値を返す。
  • ユーザに対して認可させない理由を詳細に伝えたい場合, AuthorizationException をスローする。

第1引数は必ず Authenticatable になる,但し…

以前,認証の Guard に関する説明で以下のように述べた。

  • shouldUse() メソッドでは,ユーザ取得に成功した場合のみ $this->userResolver を上書きしている。

Gate は,このメソッドを使って Authenticatable を取得し,第1引数に渡している。但し,場合によってはユーザ認証していないリクエストに対しても認可処理を行いたいというケースがあるだろう。

素直に Ability や Policy を登録した場合,ゲストに対してはメソッドを一切実行せずに終わるが,例外がある。第1引数が以下を満たす場合だ。これは非常に複雑だが, Gate のソースを深く読んでいくと分かる。

/**
 * Determine if the given parameter allows guests.
 *
 * @param  \ReflectionParameter  $parameter
 * @return bool
 */
protected function parameterAllowsGuests($parameter)
{
    return ($parameter->getClass() && $parameter->allowsNull()) ||
           ($parameter->isDefaultValueAvailable() && is_null($parameter->getDefaultValue()));
}
  • 型としてクラスが指定されており,かつそれが null 許容されている
  • デフォルト値があり,かつそれが null である

つまり,以下のような場合だ。このいずれかの条件を満たす場合,認証していなくても認可を通すことが可能になる。

public function action(?User $user, Post $post);
public function action($user = null, $post = null);

実装側に制約が与えられる引数は第1引数のみである。第2引数以降は呼び出し側が与えるものになる。

認可メソッド呼び出しのルール

Gate::authorize() (および類似するメソッド) は, $ability $arguments という引数を取る。しかしこの部分だけを見ても,使い方がさっぱりわからない。 $arguments ってなんだよ。

/**
 * Determine if the given ability should be granted for the current user.
 *
 * @param  string  $ability
 * @param  array|mixed  $arguments
 * @return \Illuminate\Auth\Access\Response
 *
 * @throws \Illuminate\Auth\Access\AuthorizationException
 */
public function authorize($ability, $arguments = [])
{
    $result = $this->raw($ability, $arguments);

    if ($result instanceof Response) {
        return $result;
    }

    return $result ? $this->allow() : $this->deny();
}

コードを追いたいが,あまりにも複雑なので,要所だけ端折って掲載する。

Gate::define() または Gate::resource() で登録した Ability を呼び出す場合

こちらはシンプルで, 定義した $ability の第2引数以降をそのまま $arguments として与えるだけでよい。

Gate::define('update-post', function ($user, $post) {
    /* ... */
});
Gate::define('update-comment', function ($user, $post, $comment) {
    /* ... */
});
Gate::authorize('update-post', [$post]);
Gate::authorize('update-comment', [$post, $comment]);

第2引数が配列ではない場合,自動的に配列でラップされるので,以下のように書くこともできる。但し,第3引数以降は拾ってくれないことに注意。

Gate::authorize('update-post', $post);

// これはダメ!
// Gate::authorize('update-comment', $post, $comment);

Gate::policy() で登録した Policy を呼び出す場合

こちらがやや複雑になる。

一般形

  • 認可メソッド名をそのまま $ability とする。
    • 但し, camelCase 表現のポリシーメソッドは kebab-case で書いてもよい。
      Ability との一貫性の面からこちらのほうが推奨される。
  • 認可メソッドの第2引数以降を格納した $arguments の先頭に, Gate::policy() の第1引数と同じ Eloquent Model クラス名を入れる。
class PostPolicy
{
    public function update(User $user, Post $post)
    {
        /* ... */
    }
    public function reportForSpam(User $user, Post $post)
    {
        /* ... */
    }
}
class CommentPolicy
{
    public function update(User $user, Post $comment, Comment $comment)
    {
        /* ... */
    }
}
Gate::policy(App\Post::class, App\PostPolicy::class);
Gate::policy(App\Comment::class, App\CommentPolicy::class);
Gate::authorize('update', [App\Post::class, $post]);
Gate::authorize('update', [App\Comment::class, $post, $comment]);
Gate::authorize('report-for-spam', [App\Post::class, $post]);

第2引数の Eloquent Model に対応する Policy を呼び出す場合の省略形

  • 第2引数の Eloquent Model インスタンスと, Gate::policy() の第1引数として使った Eloquent Model クラス名が対応する場合, Policy クラス名を省略することができる。
class PostPolicy
{
    public function update(User $user, Post $post)
    {
        /* ... */
    }
}
Gate::policy(App\Post::class, App\PostPolicy::class);
// ↓すべて等価
Gate::authorize('update', $post);
Gate::authorize('update', [$post]);
Gate::authorize('update', [App\Post::class, $post]);

この省略形が Policy の理想とする美しい形だ。使える場合には積極的に使っていきたい。

どうやって Ability と Policy を区別して判定しているのか

もはやゴリ押しと言えるような if 分岐による。最も重要な部分を日本語コメントつきで引用する。

/**
 * Resolve the callable for the given ability and arguments.
 *
 * @param  \Illuminate\Contracts\Auth\Authenticatable|null  $user
 * @param  string  $ability
 * @param  array  $arguments
 * @return callable
 */
protected function resolveAuthCallback($user, $ability, array $arguments)
{
    // $arguments[0] がセットされていて,
    // $arguments[0] から Policy インスタンスが解決できて,
    // $ability の camelCase -> kebab-case 変換込みで対応するメソッドを見つけることができるか?
    if (isset($arguments[0]) &&
        ! is_null($policy = $this->getPolicyFor($arguments[0])) &&
        $callback = $this->resolvePolicyCallback($user, $ability, $arguments, $policy)) {
        // そのメソッドを実行するためのクロージャを返す
        // ( canBeCalledWithUser の判定などはこのクロージャの中でやる)
        return $callback;
    }

    // $ability に対応するコールバックが Gate::define() で登録され,
    // $user が null の状態でゲストを受け入れない Ability を実行してしまってエラーになる心配がないか?
    if (isset($this->abilities[$ability]) &&
        $this->canBeCalledWithUser($user, $this->abilities[$ability])) {
        // そのコールバックを返す
        return $this->abilities[$ability];
    }

    // 何もしないクロージャを返す
    return function () {
        return null;
    };
}

Authorize ミドルウェア

認可処理も認証処理同様に,明示的に自分で記述せずにミドルウェアに任せることもできる。
(但し,可読性が著しく悪化する場合は推奨しない)

前項で説明した一般形・特殊形の上にさらにルートモデルバインディングを加味すると,以下のような書き方ができる。

一般形

  • can:update,App\Post,post
  • can:update,App\Comment,post,comment
  • can:report-for-spam,App\Post,post

第2引数の Eloquent Model に対応する Policy を呼び出す場合の省略形

  • can:update,post
  • can:report-for-spam,post

Authorize ミドルウェアは以下ようなコードになっている。

<?php

namespace Illuminate\Auth\Middleware;

use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Access\Gate;

class Authorize
{
    /**
     * The gate instance.
     *
     * @var \Illuminate\Contracts\Auth\Access\Gate
     */
    protected $gate;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function __construct(Gate $gate)
    {
        $this->gate = $gate;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string  $ability
     * @param  array|null  ...$models
     * @return mixed
     *
     * @throws \Illuminate\Auth\AuthenticationException
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function handle($request, Closure $next, $ability, ...$models)
    {
        $this->gate->authorize($ability, $this->getGateArguments($request, $models));

        return $next($request);
    }

    /**
     * Get the arguments parameter for the gate.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  array|null  $models
     * @return array|string|\Illuminate\Database\Eloquent\Model
     */
    protected function getGateArguments($request, $models)
    {
        if (is_null($models)) {
            return [];
        }

        return collect($models)->map(function ($model) use ($request) {
            return $model instanceof Model ? $model : $this->getModel($request, $model);
        })->all();
    }

    /**
     * Get the model to authorize.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $model
     * @return \Illuminate\Database\Eloquent\Model|string
     */
    protected function getModel($request, $model)
    {
        return $this->isClassName($model) ? trim($model) : $request->route($model, $model);
    }

    /**
     * Checks if the given string looks like a fully qualified class name.
     *
     * @param  string  $value
     * @return bool
     */
    protected function isClassName($value)
    {
        return strpos($value, '\\') !== false;
    }
}

ミドルウェアの handle() メソッドは

public function handle($request, Closure $next, ...$args);

とされることが一般的だが,ここでは $args の1つ目を取り出して

public function handle($request, Closure $next, $ability, ...$models);

と分けていることに注意されたい。結果として,例えば can:update,App\Comment,post,comment なら

$ability = 'update';
$models = [App\Comment::class, 'post', 'comment'];

という形になる。あとはルートモデルバインディングでインスタンスを取ってくるだけなのだが,「クラス名っぽい」文字列の場合トリミングしてそのままにしていることが分かる。これは Policy を一般系の形で利用したときのための対策だ。

そして最終的に

$this->gate->authorize('update', [App\Comment::class, $post, $comment]);

となって Gate の処理が走り始める。

Authorizable

最後に, Authorizable 契約およびAuthorizable トレイトによるデフォルト実装も確認しておこう。これらは必須ではないが, App\User から利用できるヘルパーメソッド的な役割を担っている。
(ヘルパー的役割であるがゆえに,このトレイトは Auth パッケージではなく Foundation パッケージのほうに入っている)

<?php

namespace Illuminate\Foundation\Auth\Access;

use Illuminate\Contracts\Auth\Access\Gate;

trait Authorizable
{
    /**
     * Determine if the entity has a given ability.
     *
     * @param  string  $ability
     * @param  array|mixed  $arguments
     * @return bool
     */
    public function can($ability, $arguments = [])
    {
        return app(Gate::class)->forUser($this)->check($ability, $arguments);
    }

    /**
     * Determine if the entity does not have a given ability.
     *
     * @param  string  $ability
     * @param  array|mixed  $arguments
     * @return bool
     */
    public function cant($ability, $arguments = [])
    {
        return ! $this->can($ability, $arguments);
    }

    /**
     * Determine if the entity does not have a given ability.
     *
     * @param  string  $ability
     * @param  array|mixed  $arguments
     * @return bool
     */
    public function cannot($ability, $arguments = [])
    {
        return $this->cant($ability, $arguments);
    }
}

Gate::forUser() が使用されているため,気兼ねなくユーザ単位で使用することができる。また Gate::check() が使用されているため例外安全でもある。どうしてもコントローラメソッド内で認可を行いたい場合には使っていきたい。

320
306
2

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
320
306

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?