Laravelの認証/認可。Auth,Gate,Policyの再整理

アドベントカレンダーに触発されて、記事を書いたところ、ちょうど枠に空きが出たので、投稿したいと思います。
Laravelのドキュメントって結構分かりやすく書かれている方だと自分は思っているんですけど、今日はその中でもこんがらがりがちな「認証と認可」について今一度、整理したいと思います。どういうケースに使えるのかとか。
なお、「認証」の方はartisan make:authしたら概ね完成しちゃいますが、認可って便利なので、認可の説明が多いです。
実世界に置き換えて考える
技術的な話に入る前に、日本語の「認証と認可」を理解します。というのも、「認証と認可」という日本語もわからないし、英語の「Authentication、Authorization」もわからないので。。。言語って難しいですね。。。
ググって見ると、「認証と認可」について、「よくわかる認証と認可 | DevelopersIO」という素晴らしい記事がありました、ここを読めば、理解できます。さすがです、クラスメソッドさん。
一部、抜粋させていただきます。
- 認証:通信の相手が誰(何)であるかを確認すること。(例:マイナンバーカード)
- 認可:とある特定の条件に対して、リソースアクセスの権限を与えること。(例:チケット/切符の発行)
切符を買った人は電車に乗るということを許可されますけど、それが誰だって構わない。切符さえ持っていれば、誰だって乗れると。それが「認可」です。
後述していますが今回、認可の実例として、運転免許を例にしています。
「運転免許証」を持つこと自体は、あなたが誰かを確認できるものになるから「認証」を意味しますけど、「運転」という行為は、免許証持っていても飲酒してたらしてはいけませんよね。
もう一つ別の例としては、教習所で試験に合格すれば、運転免許証を発行されますが、交通違反を取り締まっている警察官は、免許の点数を操作することはできますが、免許証を発行処理はできないです。この辺が認可されていたり・されなかったりの話です。
なんとなく理解できたのではないかということで、いよいよLaravelについての話に移ります。
Laravelでは、認証が「Guard」、認可が「Gate/Policy」の3パターンありますので、順にそれぞれ触れていきます。
認証:Guard
概要
よくあるID/PASS機構のログインの仕組みの提供のようなものです。
シンプルなものだと、artisan make:auth を実行するだけで完了ですが、ここでは、もう少し複雑な認証を考えてみます。
マルチ認証
v5.4からはマルチ認証が可能です、マルチ認証を使うと、次のような複数のログインの認証の仕組みを簡単に構築できるようになります。
- 誰でも閲覧できるページ
- 会員登録したユーザが見れるマイページ(/login)
- 管理用ページ(/admin/login)
上記の例の場合、次のようなGuardが考えられます。
- member:マイページが発行された会員用のGuard
- admin:全て操作できる管理者用のGuard
マルチ認証の手順/サンプルコード
手順は長くなるので、マルチ認証の手順が詳しく説明されているページを紹介するにとどめたいと思います。
Laravel5.4でマルチ認証(userとadmin)を実装する方法 | 大分のITコンサルタント | 高橋商店
ざっくりと、手順としては、
- usersテーブルを参考に、Adminユーザ用のマイグレーション作成し、
artisan db:migrateする。 - App/Userを参考にApp/Adminモデル作成する。
- config/auth.php にて、’defaults’,’guards’,’providers’,’passwords’の設定項目を変更。(admin用の設定を追加)
- ログイン用のcontrollerとviewを用意。
artisan make:authで作成されたUserのLoginControllerなどを複製するなど、適宜編集する。
class AdminLoginController extends Controller
use AuthenticatesUsers;
protected $redirectTo = '/admin/dashboard';
protected $loginPath = '/admin/login';
protected function guard()
{
return Auth::guard('adminuser');
}
...
```
以上で、ひとまずの設定は完了です。
あとは、routes/web.phpにて、次の条件で設定していきます。
- 会員認証が必要なページ:’middleware’ => ‘auth:user’
- 管理者認証が必要なページに:’middleware’ => ‘auth:admin’
(config/auth.phpで設定したguard名を用います)
こんな感じで使えます。
//特定のGuardからログアウトしたい時
\Auth::guard("ガード名")->logout();
//とにかく特定のGuardでログインさせたい時
\Auth::guard("ガード名")->loginUsingId(<<$user_id>>);
以上が、Guardです。
つい先日発売された「PHPフレームワーク Laravel Webアプリケーション開発」に、認証のインターフェース周りについてすごく詳しく書かれていました。artisan make:authのシンプルな構成だけで終わらないログイン機構はどうしたら良いか、すごく参考になります。
認可(単体):Gate
概要
認可では、特定の条件を定義しておくことで、その条件に許可する/許可しないなどの処理の制御ができるようになります。
認可には、GateとPolicyがありますが、Gateは単体で、Policyは複数です。
Real World:自動車の運転
前例に出した、「自動車の運転」を例に、Gateを考えてみます。
運転しても良い/してはいけないケースというのは、例えば次のようなケースが考えられます。
Case1 成功ケース
- 条件:運転免許所持している
- 制御:OK(運転しても良い)
Case2 エラーケース1
- 条件:運転免許所持していない
- 制御:NG(運転してはいけない)
Case3 エラーケース2
- 条件:お酒を飲んだ
- 制御:NG(運転してはいけない)
サンプルコード
上記の例を、実際にサンプルコードにて組んでみます。(動作するかどうか試していません)
Userモデル修正(マイグレーションも修正してください)
usersテーブルにlicenseとdirinkedのフラグを追加し、Userモデルを修正します。
また、ユーザが運転できるかどうかを判別するために、$user->canDrive()で、判別できるようにします。
App/User.php
...
$fillable = [
...
'license',
'drinked',
]
/**
* 運転できるか
*/
public function canDrive()
{
return $this->license && !$this->drinked
}
...
Gateを定義
Gateを定義します。ここでは仮にcan_driveという名称で定義しています。
先に設定したUserモデルの$user->canDrive()の結果がtrueなら、
このGateを通過できる(権限がある)という意味づけです。
App/Providers/AuthServiceProvider.php
...
public function boot()
{
...
\Gate::define('can_drive', function(\App\User $user){
return $user->canDrive();
});
...
Controllerにて実装
それでは、Gate定義が終わりましたので、適当に作成したcontrollerにて、テストしていきます。
$this->authorize('Gate定義名');とすることで、先に定義したGate、can_driveの条件を通過できるか(権限があるか)のチェックできます。
エラーだった場合は、AccessDeniedHttpExceptionが返されます。
ここでは、先に想定した3つのケースのrouteを用意しています。
- DriveController@case_success
- DriveController@case_lost_license
- DriveController@case_drinked
App/Http/Controllers/DriveController.php
...
use App\Http\Controllers\Controller;
use App\User;
...
class DriveController extends Controller
{
...
// 成功
public function case_success(User $user)
{
$user->license = true;
$user->drinked = false;
$this->authorize('can_drive');
return 'success';
}
// 失敗(運転免許忘れた。酒は飲んでないけどエラー)
public function case_lost_license(User $user)
{
$user->license = false;
$user->drinked = false;
$this->authorize('can_drive'); // throw AccessDeniedHttpException
return 'error';
}
// 失敗(免許証持ってるけど、酒飲んだからエラー)
public function case_drinked(User $user)
{
$user->license = true;
$user->drinked = true;
$this->authorize('can_drive');
$this->authorize('can_drive'); // throw AccessDeniedHttpException
return 'error';
}
...
認可(複数):Gate
特定のリソース(Model)に対して、特定の動作を許可したりしなかったりする、Gateのまとまりのようなもので、複数定義する必要があるGateを管理しやすくすることができます。
PolicyでできることはGateでもできます。Policyが複雑と感じれば、Gateで定義するのもありです。
次のようなCRUDをまとめて定義できます。
- Create:可
- Read(view):可
- Update:不可
- Delete:不可
Real World:運転免許証の発行、更新など
前例に出しましたが、運転免許の例で、運転免許の発行などについて考えてみます。
運転免許証のCURD(確認、取得、更新)を操作できるケースを考えると、次の3パターンの利用者がと思いつきます。
- 一般の車利用者
- 運転免許試験場(政府)
- 警察官
上記の3つの属性に対してそれぞれ、運転免許証というリソース(モデル)に対しての、CRUDを具体的に見ていきます。
- 属性:一般の車利用者
- Create:NG。自分で勝手に発行できない。
- Read(View):OK。自分の運転免許証を見ることはできる。
- Update:NG。自分で勝手に免許証の情報を書き換えれない。(申請が必要)
- Delete:NG。紛失してしまう事はありえますが、この例では削除できないとします。
- 属性:運転免許試験場(政府)
- Create:OK。誰に対しても(試験合格している人に)免許証を発行できる。
- Read(View):OK。発行されている運転免許証であれば誰のでも確認できる。
- Update:OK。変更できる。(ユーザからの住所情報変更など)
- Delete:OK。停止できる。(ユーザから免許証の停止を希望され流など)
- 属性:警察官
- Create:NG。免許書を発行できない。
- Read(View):OK。運転手に免許証を見せてくださいと頼むことができる。
- Update:OK。違反した運転手の免許証を減点できる。
- Delete:OK。違反した運転手への減点の結果、最終的に取り消しさせルことができる。
Policyを使うことで、上記のような、属性ごとのCRUDを管理できるということです。
サンプルコード
ここでは、雑なサンプルですけど、次のようなroleをUserモデルに与えれる前提とします。
$user->role = 'normal'; // 一般の車利用者 $user->role = 'government'; // 運転免許試験場(政府) $user->role = 'police'; // 警官
この例の場合は、role="government"は全ての権限があるので、
ポリシーフィルター(before filter)などを使えば簡略化できますが割愛します。
Userモデル
Userに、ユーザ属性を割り当てます。
App/User.php
class User extends Authenticatable
{
...
const ROLE_NORMAL = 'normal';
const ROLE_GOVERNMENT = 'government';
const ROLE_POLICE = 'police';
protected $fillable = [
'name', 'email', 'password','role'
];
public function isGovernment(){
return $this->role == self::ROLE_GOVERNMENT;
}
public function isPolice(){
return $this->role == self::ROLE_POLICE;
}
...
}
Userマイグレーション
Userモデルに変更を行ったので、マイグレーションも変更が必要です。
デフォルトは、一般ユーザです。
...
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->string('role')->default(\App\User::ROLE_NORMAL);
$table->rememberToken();
$table->timestamps();
});
}
License モデル
免許証に当たるLicenseのモデルを定義します(新規作成)。
ここでは、ユーザID、住所、(事故の)点数があります
App/License.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class License extends Model
{
protected $fillable = [
'user_id','address', 'point',
];
public function User(){
$this->hasOne(User::class);
}
}
License マイグレーション
マイグレーションを用意します
class CreateLicensesTable extends Migration
{
...
public function up()
{
Schema::create('licenses', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id');
$table->string('address')->nullable();
$table->integer('point')->default(15);
$table->timestamps();
});
}
...
ポリシーファイル作成
このファイルで、CURDに対しての認証処理を記述していきます。
method名は、CURDを実現するために、view、create、update、deleteとしました。
新規作成
App/Policies/LicensePolicy.php
namespace App\Policies;
use App\User;
use App\License;
use Illuminate\Auth\Access\HandlesAuthorization;
class LicensePolicy
{
use HandlesAuthorization;
public function view(User $user, License $license)
{
// 誰でも確認できる
return true;
}
public function create(User $user)
{
// 試験場のみが免許証を発行できる
return $user->isGovernment();
}
public function update(User $user, License $license)
{
// 試験場(政府)か警察官なら更新できる
return $user->isGovernment() || $user->isPolice();
}
public function delete(User $user, License $license)
{
// 試験場(政府)なら無条件で。警察官ならその免許証が0点になれば免許取り消しになる。
return $user->isGovernment() || ($user->isPolice() && $license->point <= 0);
}
}
用意したポリシーを登録
...
use App\License;
use App\Policies\LicensePolicy;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
...
License::class => LicensePolicy::class,
];
...
最後にテスト用のコントローラ
(かなり冗長でひどいですけど、とりあえず、ぱっと動作確認できるものを用意したかったので)
`$this->authorize(‘Gate名’);`で認可の判断処理が行われます。
namespace App\Http\Controllers;
use App\License;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class LicenseController extends Controller
{
public $user;
public $license;
private function setupNormal(){
$this->user = User::firstOrCreate([
'name' => 'normal',
'email' => 'normal@example.com',
'password' => 'xxxxxxx',
'role' => User::ROLE_NORMAL,
]);
\Auth::loginUsingId($this->user->id);
}
private function setupGovernment(){
$this->user = User::firstOrCreate([
'name' => 'government',
'email' => 'government@example.com',
'password' => 'xxxxxxx',
'role' => User::ROLE_GOVERNMENT,
]);
\Auth::loginUsingId($this->user->id);
}
private function setupPolice(){
$this->user = User::firstOrCreate([
'name' => 'police',
'email' => 'police@example.com',
'password' => 'xxxxxxx',
'role' => User::ROLE_POLICE,
]);
\Auth::loginUsingId($this->user->id);
}
private function attachLicense(){
$this->license = License::firstOrCreate([
'user_id' => $this->user->id,
'point' => 15,
'address' => "日本のどこそこ",
]);
}
// 免許のポイントを0に
private function pointZero(){
$this->license->point = 0;
$this->license->save();
}
/**
* 一般ユーザ:閲覧権限あり
*/
public function indexNormal(){
$this->setupNormal();
$this->attachLicense();
$this->authorize('view', $this->license);
return '成功';
// // もしくは
// if ($this->user->can('view', $this->license)) {
// return '成功';
// }
// return '失敗';
}
/**
* 一般ユーザ:作成権限なし
*/
public function createNormal(){
$this->setupNormal();
$this->authorize('create', License::class); // ここで終わり
return '成功';
}
/**
* 一般ユーザ:更新権限なし
*/
public function updateNormal(){
$this->setupNormal();
$this->attachLicense();
$this->authorize('update', $this->license); // ここで終わり
return '成功';
}
/**
* 一般ユーザ:削除権限なし
*/
public function deleteNormal(){
$this->setupNormal();
$this->attachLicense();
$this->authorize('delete', $this->license); // ここで終わり
return '成功';
}
/**
* 試験場:閲覧権限あり
*/
public function indexGovernment(){
$this->setupGovernment();
$this->attachLicense();
$this->authorize('view', $this->license);
return '成功';
}
/**
* 試験場:作成権限あり
*/
public function createGovernment(){
$this->setupGovernment();
$this->authorize('create', License::class);
return '成功';
}
/**
* 試験場:更新権限あり
*/
public function updateGovernment(){
$this->setupGovernment();
$this->attachLicense();
$this->authorize('update', $this->license);
return '成功';
}
/**
* 試験場:削除権限あり
*/
public function deleteGovernment(){
$this->setupGovernment();
$this->attachLicense();
$this->authorize('delete', $this->license);
return '成功';
}
/**
* 警官:閲覧権限あり
*/
public function indexPolice(){
$this->setupPolice();
$this->attachLicense();
$this->authorize('view', $this->license);
return '成功';
}
/**
* 警官:作成権限なし
*/
public function createPolice(){
$this->setupPolice();
$this->authorize('create', License::class);
return '成功';
}
/**
* 警官:更新権限あり
*/
public function updatePolice(){
$this->setupPolice();
$this->attachLicense();
$this->authorize('update', $this->license);
return '成功';
}
/**
* 警官:削除権限条件付きであり。この場合はエラー
*/
public function deletePolice(){
$this->setupPolice();
$this->attachLicense();
$this->authorize('delete', $this->license); // ここで終わり
return '成功';
}
/**
* 警官:削除権限条件付きであり。免許のポイントが0だと許可されるPolicy設定にしてある
*/
public function deletePoliceAfterZeroPoint(){
$this->setupPolice();
$this->attachLicense();
$this->pointZero();
$this->authorize('delete', $this->license);
return '成功';
}
}
こんな感じでPolicyの動作の検証ができます。
先に記述したPolicyファイル(App/Policies/LicensePolicy.php)のPolicyのメソッドは、view/create/update/deleteだけでしたけど、public function hoge_gate(User $user, License $license){ /*処理*/ } などとすると、Gate名は、hoge_gateとなり、コントローラで、$this->authorize('hoge_gate', $this->license);で、hoge_gateの認可の判断処理が行われます。
Conclusion
Policyについてはあんまり良い例じゃないような気もしていますが、良しとします。
ここで紹介したPolicyのサンプルファイルについては、githubにおいています。適当に参考ください。
https://github.com/tomothumb/laravel-policy-note
Eloquentを継承しないクラスのPolicy
この記事で紹介しませんでしたが、Policyの対象となるリソースは、Eloquentを継承をしていなくても良いです。単一モデルの動作ではなくて、複数のモデルにまたがるような複雑な処理を行っているサービスがあったとして、利用者の状態や環境によって、認可したりしなかったりということができそうですね。githubに簡単なサンプル置いてるので、イメージつかんでもらえれば。
https://github.com/tomothumb/laravel-policy-note/blob/master/app/Service/SampleService.php
https://github.com/tomothumb/laravel-policy-note/blob/master/app/Policies/SamplePolicy.php
https://github.com/tomothumb/laravel-policy-note/blob/master/app/Http/Controllers/SampleController.php
GuardとGateとPolicyの違いをまとめて見ました。
Policyについては、Githubにて、実際のサンプルコードを用意してますので確認できます。
余談ですが、サンプルを作って思ったのですが、テストコードで解説するべきでした・・・
readoubleのリリースノートを見ましたが、マルチ認証の実装は5.2からではないですか。
おっしゃられる5.4以前からも提供されているみたいですね。
5.2のリリースでは5.1の時からサポートされてるとありますね。
5.4以前で、動作検証おこなっていないので、記事はこのままにしておこうかと思います!
“`
Laravel 5.2 continues the improvements made in Laravel 5.1 by adding multiple authentication driver support, implicit model binding, simplified Eloquent global scopes, opt-in authentication scaffolding, middleware groups, rate limiting middleware, array validation improvements, and more.
“`
https://laravel.com/docs/5.4/releases#laravel-5.2