Laravel5.7 [API開発]で権限設定をPolicyに任せて幸せになろう
LaravelでAPIを作っていると、例外周りの処理を良い感じにしてくれないので非常に大変です。 Policyをそのまま使っても403ページが返ってしまいますし、エラーメッセージの変更も困難です。 今回は権限判定をPolicyに移植して、かつ任意のエラーメッセージをjsonで返せるようにした手順をご紹介します。
FormRequestについての記事もあります。 nakka-k.hatenablog.com
- Policyとは
- 今まではこんな書き方をしていた
- Policyを作成する
- Policyを組み込む
- エラーハンドリングのためにLaravelのコードリーディングをする
- エラーをjsonで返すようにする
- 最終的にできたコード
- PolicyとFormRequestを併用
- まとめ
- 参考
Policyとは
Laravelには特定のモデルやリソース、処理に対して権限を判定し認可・非認可する方法があります。 その方法には大きく分けてGateとPolicyの2つがありますが、今回はPolicyについてのみ説明します。
Policyとは、モデルなどのデータに対してユーザーが行った動作を認可・非認可するために使われる仕組みです。 例えばPostモデルを編集する権限はPostを投稿したユーザーのみにしかない、といった権限判定に使われます。
今まではこんな書き方をしていた
※ソースコードは一部改変しています
ユーザーそれぞれが持つ本棚に対して設定を変更する処理を書いているコントローラーです。 コントローラー内で編集先のユーザーIDとログインしているユーザーIDを比較して、許可されていない編集だと判定した場合に403のjsonレスポンスを返しています。
class UserBookController extends Controller { public function update(Request $request, $user, $userBookId) { // 認可チェック if(auth()->guard('api')->id() != $user->id){ return response()->json( [ 'status' => 403, 'errors' => ['自分以外の本棚を編集することはできません。'] ], 403 ); } // success authorized ...... } }
コントローラー内でいちいち認可チェックを書いていてはコントローラーが肥大化しますし、コードを間違えて本来許可されていない動作を許してしまう可能性が増えてしまいます。 やはり認可は認可だけ別の場所に移動させてしまいたいですね。
Laravelでモデル操作の認可に関する処理を書く場合はPolicyを使うべし、と公式ドキュメントに書かれていたのでPolicyを作っていきましょう。
Policyを作成する
Policyはモデルと1対1で結びつく物です。 基本的なCRUD操作に対してそれぞれ認可判定を書けるようになっています。
ファイル生成
今回はUserBook
というモデルに対してPolicyを生成しますので--model
オプションでモデルを指定しています。
実際に生成されたメソッドを全て使うわけではありませんが、最初に全体を見ていた方が分かりやすいので生成した後に使わないメソッドを消していく手法を取っています。
$ php artisan make:policy UserBookPolicy --model=UserBook
もちろん--model
オプションは付けずとも作ることができます。
コマンドを実行するとapp/Policies/
が作成され、その下にこのようなファイルが作成されます。
思った以上にメソッドが多いですが簡単にまとめると以下のような役割になっています。
- view: 閲覧
- create: 作成
- update: 更新
- delete: 削除
- 論理削除を実装している場合、物理削除ではなく論理削除する時
- restore: 論理削除状態から復元する時
- 論理削除を実装している場合のみ
- forceDelete: 物理削除
- 論理削除を実装している場合のみ
処理記述
今回はupdate
を実装していきます。
Policyの各メソッドはBoolean返す必要があります。
認可だとtrue
、非認可だとfalse
を返すため条件分をそのまま戻り値に書いてあげましょう。
もちろんif
で制御しても良いです。
public function update(User $user, UserBook $userBook) { return $user->id == $userBook->user_id; }
次はPolicyをモデルに紐づけて認可チェックをしてくれるように設定しましょう。
Policyを組み込む
まずapp/Providers/AuthServiceProvider.php
を編集します。
このファイルには$policies
という連想配列を持つ変数があります。
そこにモデルとPolicyの対応づけを追加しましょう。
protected $policies = [ 'App\Model' => 'App\Policies\ModelPolicy', 'App\UserBook' => 'App\Policies\UserBookPolicy' ];
そして追加したPolicyを反映させるために、コントローラーの__construct
でミドルウェアの設定をします。
今回はコントローラーでミドルェアの設定をしますがどこで設定しても構いません。(参考)
class UserBookController extends Controller { public function __construct(){ $this->middleware('can:update,userBook')->only('update'); }
これによりUserBook
モデルのDI時にUserBookPolicy
の認可チェックを走らせ、場合によって403のレスポンスを返してくれるようになります。
ですがここで問題が発生します。 基本的にLaravelの例外処理はhtmlページを返すようになっています。 これではAPIとして問題がありますので、次はjsonを返すようにしていきましょう。
エラーハンドリングのためにLaravelのコードリーディングをする
はい。まず結論から申しますとPolicyにレスポンスを動的に記述するメソッドは見つかりませんでした。 いろいろ調べてもよく分からなかったので軽くLaravelのコードリーディングをしてみましょう。
Policyのファイルを見てみるとuse HandlesAuthorization;
というトレイトをuseしている行があります。
Policyクラスは継承などをしていないようなので、ほぼ全ての動作がこのuse HandlesAuthorization;
に集約されていそうです。
このトレイトはIlluminate\Auth\Access
ネームスペースのようなので、まずそのファイルを見てみましょう。
allow
とdeny
という2つのメソッドだけありますね。
なんとなく認可・非認可に使われるメソッドのようですし、失敗した時はdenyにエラーメッセージが渡されているようです。
これだけでは全然わかりませんね。
まずはdeny
メソッドの戻り値であるAuthorizationException
を見てみましょう。
なんというか何も書いていませんね。 でもまあException(例外)であることだけは分かります。
ではもう一度HandlesAuthorization
に戻って、次はallow
メソッドの戻り値であるIlluminate/Auth/Access/Response
を見ていきましょう。
戻り値のラッパーでしかなさそうですね。 とりあえずスルーしましょう。
んー、なんだか遡れそうな先がなくなりました。
では同じnamespaceにあるまだ見ていないファイルも見てみたいので同じディレクトリにあるIlluminate/Auth/Access/Gate
も読んでみましょう。
(ファイルが大きすぎるのでリンク先でご覧ください。)
とりあえずHandlesAuthorization
で使われていたallow
やdeny
の呼び出しとかがないか調べてみましょう。
なんだかauthorize
というメソッドに使われている痕跡がありますね。
レスポンスの型も同じですね。
/** * 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(); }
ここからもいろいろ遡れますがあまり有益な情報は得られませんでした。 私のLaravelのコードリーディング力の無さが露見しますね。
一番有益な情報もありました。
AuthorizationException
にメッセージを渡してthrow
すればなんとなく上手くいきそうな気がします。
ではその方針で進めていきます。
エラーをjsonで返すようにする
まずはjsonを返すようにしましょう。
Policyで非認可されればAuthorizationException
が帰ってくるようなので、その例外をキャッチしてjsonを返すように処理を書きます。
レスポンスの最終的なエラーハンドリングを変更するにはapp/Exceptions/Handler.php
のrender
メソッドに追記していきましょう。
public function render($request, Exception $exception) if ($exception instanceof AuthorizationException) { return response()->json([ 'errors' => [$exception->getMessage()] ], 403); } return parent::render($request, $exception); }
例外である$exception
の型を判定してAuthorizationException
であればjsonを返すように記述しましょう。
jsonにエラーメッセージを含めるために$exception->getMessage()
をレスポンスに入れましょう。
ここで返されるメッセージはHandlesAuthorization
のdeny
メソッドのデフォルト引数だった'This action is unauthorized.'
です。
次はエラーメッセージを動的に変更できるようにしましょう。
Policyのファイルを編集します。私の場合app/Policies/UserBookPolicy.php
ですね。
以前は条件文を返すだけだったのでBooleanでしたね。
ここでAuthorizationException
を返すようにしてしまいます。
public function update(User $user, UserBook $userBook) { if ($user->id == $userBook->user_id) { return true; } throw new AuthorizationException('自分以外の本棚を編集することはできません。'); }
認可する場合は今まで通りtrue
を返してあげますが、false
を返していたタイミングではAuthorizationException
をthrow
します。
ここで投げられたAuthorizationException
は先ほど書いたエラーハンドリングメソッドであるrender
メソッドでキャッチされます。
AuthorizationException
に渡した引数が$exception->getMessage()
で呼び出されるメッセージになります。
実際のレスポンスを見てみましょう。
{ "errors": [ "自分以外の本棚を編集することはできません。" ] }
想定通りのレスポンスになっていますね! ということはこれで、jsonを返しつつエラーメッセージも動的に変更することができました!!
最終的にできたコード
https://gist.github.com/NAKKA-K/3d296424fd0c2568ceb6f5d91157c8a5
PolicyとFormRequestを併用
今回の記事では殆ど出てきませんでしたがFormRequestと言う、POSTなどで送られてきたデータをバリデーションしてくれる機能があります。 FormRequestとPolicyを併用した場合、しっかりとPolicyで権限をチェックした後にリクエストのバリデーションをしてくれます。 なので安心してFormRequestとPolicyを併用してください。
FormRequestの導入方法は以下の記事をご覧ください。
まとめ
やはり権限周りのチェックはかなり重要な機能です。 確実に権限判定の処理を書くためにPolicyに責任分離し、しっかりと実装しておきましょう。
Policyに分離することで責任分離もできますし、コントローラーの記述量がかなり少なくできます。 どんどん使っていきましょう。