はじめに
いつもどおり,以下のマニュアルの一通りの確認を先に。
「認証」と「認可」の違い
これを分かっていないと話にならないので念の為記載。
概念の違い
名称 | 説明 |
---|---|
認証 Authentication |
|
認可 Authorization |
|
ステータスコードや例外クラス名の違い
名称 | HTTP Status |
Application Exception | HTTP Exception |
---|---|---|---|
認証 Authentication |
401 Unauthorized |
AuthenticationException |
UnauthorizedHttpException |
認可 Authorization |
403 Forbidden |
AuthorizationException |
AccessDeniedHttpException |
- アプリケーション例外は Laravel フレームワークで定義されているもので,HTTPに直接関係ない抽象的な部分ではまずこれをスローすべき。
- HTTP 例外は Symfony フレームワークで定義されているもので,**
Handler::render()
でアプリケーション例外から変換して生成する**のが一般的。
401 Unauthenticated
のほうがどう考えても正しいが,そうなってないのには歴史的な理由があるのだろうか…?
認証のアーキテクチャ
-
Auth
ファサードあるいはコンテナから"auth"
のエイリアス名でアクセスできる,認証サービスのルートオブジェクトがAuthManager
である。ここが認証サービスの中核となる。 - Guard が認証手段ごとの実装として存在し,ロジックの多くはここに集約される。 このクラスは
Guard
契約 を実装しなければならない。 - ユーザを取得してくる部分だけが UserProvider に抽象化して分離される。このクラスは
UserProvider
契約 を実装しなければならない。 - ユーザとして用いるクラスは
Authenticatable
契約 を実装しなければならない。
認証済みのユーザを取得する場合
デフォルトの認証手段である SessionGuard
を利用する場合を例として示す。
新しくユーザを認証する場合
デフォルトの認証手段である SessionGuard
を利用する場合を例として示す。
※ 本当は 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 のインスタンスを返す。- 引数として渡されたガード名,もしくは
getDefaultDriver()
メソッドでデフォルトのガード名を取得 - 生成済み 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通信が絶対に起こってはいけない場所で重宝する。例えば管理者にだけ見える属性を定義したい場合,以下のような書き方をするとよいだろう。
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\User
に Authenticatable
トレイト を使ってデフォルトで実装されている。
メソッド | 説明 |
---|---|
getAuthIdentifierName |
id など,識別子の名前を返す。 |
getAuthIdentifier |
識別子の値を返す。 |
getPassword |
パスワードハッシュの値を返す。 |
getRememberToken |
自動ログイントークンの値を返す。 |
setRememberToken |
自動ログイントークンをセットする。 |
getRememberTokenName |
remember_token など,自動ログイントークンの名前を返す。 |
厄介なのが, StatefulGuard
を利用しなくても Authenticatable に自動ログイントークンの実装が強制されていることだ。認証をすべて外部サービスに委譲している場合などには余計なお節介だ。こういう場合,以下のようなトレイトを作って App\User
にミックスインするといいだろう。
<?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 の自作を検討
- はい →
Auth::viaRequest('custom-token', function (Request $request): ?User {
return User::where('token', $request->token)->first();
});
Auth::viaRequest('custom-token', function (Request $request, UserProvider $provider): ?User {
return $provider->retrieveByCredentials($request->only('token'));
});
なお,ライブラリとして Guard を実装する場合にはできるだけ UserProvider の仕組みに乗っかるべきだが,アプリケーションを作るだけなら過度な抽象化に付き合う必要はなく, UserProvider を使わない Guard が存在しても特に問題はない。
Authenticate
ミドルウェア
ここまで認証のための処理を見てきたが,リクエストを処理する際に認証処理を呼び出す起点になるのが Authenticate
ミドルウェア である。あらかじめ定義された Guard が順番に認証を試み,成功した時点で Authenticatable を取得して処理を続行,という動きをする。以下の図のようなイメージだ。
では,この実装を読んでみよう。
<?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
を上書きしている。- このクロージャは
Gate::resolveUser()
やRequest::user()
で使用されている。
- このクロージャは
コントローラメソッドにて,ファサード形式で
public function something(Request $request)
{
$user = Auth::user();
/* ... */
}
なんて使い方をしたりするが,上記ゆえに実は引数の Request
をそのまま流用して
public function something(Request $request)
{
$user = $request->user();
/* ... */
}
なんて形でも取れたりする。
では,認証はこのぐらいにしておいて,認可に進もう。
認可のアーキテクチャ
認証したユーザに対し,具体的にどんな動作を許可できるかどうかを定義するのがこのレイヤーだ。
Gate
が認可サービスのルートオブジェクトになり,一連の処理の中核となる。同名のファサードに対応するのもこのクラスだ。そして,以下のような処理をすべて担う。
- 単一の権限判定関数として, Ability を
Gate::define()
で登録。 - Eloquent Model に対する複数の権限判定メソッドの集合であるクラスとして, Policy を
Gate::policy()
で登録。 -
Gate::authorize()
で権限名称から使用すべき関数・メソッドを特定して,認可処理を行い,失敗した場合にはAuthorizationException
をスローする。
Gate
の概要
Gate
を自前で実装することはほぼ無いが,一応 Gate
契約 は存在する。使い方を確認するにはちょうどいいので,まずこれを確認しよう。
登録用メソッド
メソッド | 説明 |
---|---|
define |
コールバックを Ability として登録する。 |
policy |
クラスを Policy として登録する。 |
before |
認可処理の前処理を登録する。null 以外の値を返すと認可処理をスキップできる。 |
after |
認可処理の後処理を登録する。 |
App\Providers\AuthServiceProvider
の registerPolicies()
メソッドで自動的に 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; } }
著者自身も当初はこの実装が最も正しく美しいと考えていたが,
-
Gate::authorize()
は論理値だけでなくResponse
を返してもよい -
Gate::authorize()
があっさりとAuthorizationException
をスローしている -
Gate::allows()
Gate::denies()
などはAuthorizationException
をキャッチして例外安全にしている
ということを踏まえると,極端な話こんな実装もアリといえばアリだろうか。
<?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::resolveUser()
やRequest::user()
で使用されている。
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 との一貫性の面からこちらのほうが推奨される。
-
但し, camelCase 表現のポリシーメソッドは kebab-case で書いてもよい。
- 認可メソッドの第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()
が使用されているため例外安全でもある。どうしてもコントローラメソッド内で認可を行いたい場合には使っていきたい。