Laravel5でMultiAuth（userとadminでテーブル分ける）


Laravelには便利な認証機構が存在しますが、標準ではusersテーブルのみを使ってユーザーを認証します。
実際の開発ではadministratorsとusersを分けたいことはよくあることなので、テーブルを分けた認証をしてみます。

Laravel4の時代は定番のパッケージなどがあったようですが、Laravel5には対応していないようで、仕方ありません。

###追記

[5.1対応のパッケージを見つけたので、その使い方を書きました。](http://qiita.com/zaburo/private/bc699185b6de12c0413d)こちらの方がオススメです。

###参考

ちょうど、[目的とする内容の記事](http://blog.comnect.jp.net/blog/95)を見つけたので参考にさせていただきました。

###やりたいこと
・認証用のユーザーをusersとadministratorsでテーブルを分けて、それぞれ認証する。
・一般ユーザーは、標準のusersおよび標準ドライバを利用。
・userの認証はAuth::attempt(['email'=>$email,'password'=>$password])ですが、これに加え、
・adminの認証はAuth::driver('admin')->attempt(['email'=>$email,'password'=>$password])とできるようにする。

###やること

やることは、administratorsテーブルの構造をこらなければ、大きくは下記の２つ。

・UserProviderの実装で利用するユーザー（AdminUser）を作る（IDを変えるため）
・UserProviderインターフェースを継承したプロバイダを作成
・プロバイダに設定を記述

正直、テーブルやデータの用意など、下準備の方が大変です。
Laravel4では文字通り、UserProviderInterfaceを継承したが、Laravel5では、UserProviderという名前になった。が、機能はほぼ同じようです。

###前提条件

・administratorsテーブルの構造は、原則usersと同じですが、administratorを認識するIDとして、administrators_idを追加する。

#注意と継続調査事項

usersとadministratorsとテーブルを分けても、認証に使うID(identifier)の重複は結局のところ許されないようです（動作はしますが、同じIDのユーザ-がいるとおかしな動作をする）。
私のやり方がわるいだけかもしれませんが。

つまり、

・Auth::attempt()
・Auth::driver('admin')->attempt();

で、誰かがログインした際。見に行くDBやカラムは違っても、結局identifierとして同じ値が取得された場合、どちらもログインした状態とみなされるようです。

そのため、usersのidと、administrators_idの内容が重複しないように実装が必要です。すなくとも、ここで実装する方法ではそうなります。

ドライバが違っても、認証のためにsessionに書き込むIDは同じため？でしょうか。ソース読んでみます。
他のmulti auth系のパッケージとか見ても、IDの重複を気にしているようなコードはないようだけど・・・・。私の勘違いならいいのですが。

認証でrole的な機能が必要な場合、

・１つのテーブルでroleを追加し、attempt()でroleも含めて認証する
・複数のテーブルで、identifierとなるカラムの値を重複させないように実装する
・Sessionの書き込み、読み込みを工夫しidentifierの重複を防ぐ（Guardを拡張？）

で対応かな。正直、あまり複雑になってくると、毎度おなじみフレームワークの意味が無いなあ。

###下準備

####administratorsテーブルの準備

migrationファイルを作成する。

```bash
php artisan make:migration create_administrators_table --table=administrators
```

などとする。
とりあえず、administrator_idは、stringにして、admin_プリフィック+uniqid()で生成することにする。なんか釈然としない。

database/migrations/XXXXX_create_administrators_table.php

```php
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAdministratorsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('administrators', function(Blueprint $table)
		{
			//
			$table->increments('id');
			$table->string('administrators_id',128)->unique("admin_");
			$table->string('name');
			$table->string('email')->unique();
			$table->string('password',60);
			$table->rememberToken();
			$table->timestamps();
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('administrators');
	}

}

```

こんな感じ。

```bash
php artisan migrate
```

で、usersテーブルも一緒に作成されます。

####データの準備

users, administratorsに予めユーザーを登録しておきましょう。formを作ってもいいし、insertしてもいいですが、折角なのseederを利用します。
seederを作るartisanコメンドはないようなので、手書きします（なんで？）
なお、ここでは、Eloquent ORMを利用しているので、Administrator.php（モデル）を作成しておく。
いろいろ書いているが、基本、User.phpをコピーして、class名と$tableだけ変えればいいと思う。

app/Administrators.php

```php
<?php namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class Administrator extends Model implements AuthenticatableContract, CanResetPasswordContract {

	use Authenticatable, CanResetPassword;

	protected $table = 'administrators';

	protected $fillable = ['name', 'email', 'password'];

	protected $hidden = ['password', 'remember_token'];

}

```

seederはこんな感じ。hogeという１人のユーザーを足しておく。とりあえずidはuniqid()で生成。
database/seeder/AdministratorTableSeeder


```php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
use App\Administrator;

class AdministratorTableSeeder extends Seeder {

	/**
	 * Run the database seeds.
	 *
	 * @return void
	 */
	public function run()
	{
		
		//DB::table('administrators')->delete();
		Administrator::truncate();

		Administrator::insert([
			[
				'administrator_id'=>uniqid();
				'name'=>'hoge',
				'email'=>'hoge@test.com',
				'password'=>Hash::make('hoge')
			]
		]);

	}

}

```

seederは、

```bash
php artisan db:seed
```

で実行されるが、この時実行されるのは、DatabaseSeeder.phpのみである。そのため、DatabaseSeeder.phpにAdministratorTableSeeder.phpが実行されるように記述する。

DatabaseSeeder.php

```php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder {

	/**
	 * Run the database seeds.
	 *
	 * @return void
	 */
	public function run()
	{
		Model::unguard();

		//$this->call('UserTableSeeder');
		$this->call('AdministratorTableSeeder');
	}

}
```

パワワードはわすれずハッシュ化しておきます。でないと認証の時に一致しません。

###では、本番

やっと下準備ができたので、カスタムドライバを実装していきます。ファイルはどこに作ってもいいので、とりあえapp直下につくります。
いろいろ書いてますが、内容はほぼIlluminate/Auth/DatabaseUserProvider.phpからのパクリ。

・connectionやtable、hasher等を静的に書きなおし。
・GenericUserを返しているところを、GenericUserを継承したAdminUserを返す。
・idを見ているところを、administrator_idを見るようにする。

という感じ。

####カスタムUser

```php
<?php namespace App;

use Illuminate\Auth\GenericUser;

class AdminUser extends GenericUser
{

	public function getAuthIdentifier(){
		return $this->attributes['administrator_id'];
	}
	
}
```

####カスタムUserProviderの実装


app/AdministratorProvider.php

```php
<?php namespace App;

// use Illuminate\Auth\UserInterface;
// use Illuminate\Auth\UserProviderInterface;

use DB;
use Hash;
use Log;

use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Auth\GenericUser;

class AdministratorProvider implements UserProvider
{

	//table
	protected $table = "administrators";

	/**
	 * Retrieve a user by their unique identifier.
	 *
	 * @param  mixed  $identifier
	 * @return \Illuminate\Contracts\Auth\Authenticatable|null
	 */
	public function retrieveById($identifier)
	{
		//$user = DB::table($this->table)->find($identifier);

		$user = DB::table($this->table)
							     ->where('administrator_id',$identifier)
							     ->first();

		return $this->getGenericUser($user);
	}

	/**
	 * Retrieve a user by by their unique identifier and "remember me" token.
	 *
	 * @param  mixed   $identifier
	 * @param  string  $token
	 * @return \Illuminate\Contracts\Auth\Authenticatable|null
	 */
	public function retrieveByToken($identifier, $token)
	{
		$user = DB::table($this->table)
                                ->where('administrator_id', $identifier)
                                ->where('remember_token', $token)
                                ->first();

		return $this->getGenericUser($user);
	}

	/**
	 * Update the "remember me" token for the given user in storage.
	 *
	 * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
	 * @param  string  $token
	 * @return void
	 */
	public function updateRememberToken(UserContract $user, $token)
	{
		DB::table($this->table)
                            ->where('administrator_id', $user->getAuthIdentifier())
                            ->update(['remember_token' => $token]);
	}

	/**
	 * Retrieve a user by the given credentials.
	 *
	 * @param  array  $credentials
	 * @return \Illuminate\Contracts\Auth\Authenticatable|null
	 */
	public function retrieveByCredentials(array $credentials)
	{
		// First we will add each credential element to the query as a where clause.
		// Then we can execute the query and, if we found a user, return it in a
		// generic "user" object that will be utilized by the Guard instances.
		$query = DB::table($this->table);

		foreach ($credentials as $key => $value)
		{
			if ( ! str_contains($key, 'password'))
			{
				$query->where($key, $value);
			}
		}

		// Now we are ready to execute the query to see if we have an user matching
		// the given credentials. If not, we will just return nulls and indicate
		// that there are no matching users for these given credential arrays.
		$user = $query->first();

		return $this->getGenericUser($user);
	}

	/**
	 * Get the generic user.
	 *
	 * @param  mixed  $user
	 * @return \Illuminate\Auth\GenericUser|null
	 */
	protected function getGenericUser($user)
	{
		if ($user !== null)
		{
			//return new GenericUser((array) $user);
			return new AdminUser((array)$user);
		}
	}

	/**
	 * Validate a user against the given credentials.
	 *
	 * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
	 * @param  array  $credentials
	 * @return bool
	 */
	public function validateCredentials(UserContract $user, array $credentials)
	{
		$plain = $credentials['password'];

		// Log::debug('$plain='.$plain);
		// Log::debug('hash(plain)='.Hash::make($plain));
		// Log::debug('getAuthPass='.$user->getAuthPassword());

		return Hash::check($plain,$user->getAuthPassword());

	}

}
```

私は、DatabaseUserProvider.phpからコピーしましたが、必要なメソッドを実装（オーバライド）すればOKです。
retrieveByCredentials(array $credentials)がattempt()からの配列を受取、password以外(emai,name)を一致するユーザーを探して返し、validateCredentials(UserContract $user, array credentials)で、ハッシュ化したパスワードの一致をチェックしているようです。

#####パスワードを平文にしたい場合
パスワードを平文にしたい場合は、保存時にハッシュ化せず、ここで、if($plain == $user->getAuthPassword())とすればいいと思います（試してませんが）。

####サービスプロバイダでの記述

Laravel5から、global.phpがなくなりました。独自にサービスプロバイダを作成、登録するか、既に登録済みのプロバイダーに記述するかどちらかです。めんどいので、app/Providers/AppServiceProvider.phpに必要な記述を行います。
記述したのはboot()の部分です（registerの部分は既存）。

app/Provider/AppServiceProvider.php

```php
<?php namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\AdministratorProvider;
use Illuminate\Auth\Guard;

class AppServiceProvider extends ServiceProvider {

	/**
	 * Bootstrap any application services.
	 *
	 * @return void
	 */
	public function boot()
	{
		$this->app['auth']->extend('admin',function($app){
			return new Guard(new AdministratorProvider,$app['session.store']);
		});
	}

	/**
	 * Register any application services.
	 *
	 * This service provider is a great spot to register your various container
	 * bindings with the application. As you can see, we are registering our
	 * "Registrar" implementation here. You can add your own bindings too!
	 *
	 * @return void
	 */
	public function register()
	{
		$this->app->bind(
			'Illuminate\Contracts\Auth\Registrar',
			'App\Services\Registrar'
		);
	}

}
```

###動作の確認

これで使えるようになりました。

####ログインしてユーザー情報取得

ルートのクロージャに直接記述して動作チェック。おなじみAuth::*ファサードにAuth::driver('admin')をつけて実行します。

```php
Route::get('hoge',function(){

	//Auth::attempt(['email'=>'foo@test.com','password'=>'foo']);
	
	//認証（ログイン）
	Auth::driver('admin')->attempt(['email'=>'hoge@test.com','password'=>'hoge']);
	
	//情報取得
	$admin = Auth::driver('admin')->user();

	//ログインんできてないと$userが取得できないのでエラーになるはず
	return $admin->name;

});
```

####ログアウト

```php
Route::get('logout',function(){
	Auth::driver('admin')->logout();
});
```


###Middlewareを定義してみる

折角なので、これらのドライバを利用するミドルウエアを記述してみます。/admin以下はadmin権限が必要という感じにしたい。
ひな形は

```bash
php artisan make:middleware AdminAuthenticate
```
で作れます。

```php
<?php namespace App\Http\Middleware;

use Closure;

class AdminAuthenticate {

	/**
	 * Handle an incoming request.
	 *
	 * @param  \Illuminate\Http\Request  $request
	 * @param  \Closure  $next
	 * @return mixed
	 */
	public function handle($request, Closure $next)
	{

		if(\Auth::driver('admin')->check()){

			//Do nothing.
			
		}else{

			return "go to admin login page";

		}

		return $next($request);
	}

}
```

こんな感じ。これを、Routingで利用できるようにするためには、Kernel.phpの$routeMiddlewareに登録します。

```php
	/**
	 * The application's route middleware.
	 *
	 * @var array
	 */
	protected $routeMiddleware = [
		'auth' => 'App\Http\Middleware\Authenticate',
		'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
		'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
		'admin'=>'App\Http\Middleware\AdminAuthenticate',
		'user'=>'App\Http\Middleware\UserAuthenticate',
	];
```

これを利用するには、Routingで、

```php
Route::get('admin',['middleware'=>'admin',function(){
	return "/admin";
}]);

Route::get('admin/tools',['middleware'=>'admin',function(){
	return "/admin/tools";
}]);
```

などとします。ルートが多い場合はgroupを利用したほうがいいでしょう。下記は上記と同義。

```php
Route::group(['prefix'=>'admin','middleware'=>'admin'],function(){

	Route::get('/',function(){
		return "/admin";
	});
	
	Route::get('tools',function(){
		return "/admin/tools";
	});
	
});
```

admin/*みたいなのはないんかね。

###メモ

全てのmigrationをやり直すには、

```bash
php artisan migration:refresh
```

とする。 --seedオプションを使うとseedもやりなおしてくれるという記述を見つけたけど、動きませんでした。

あと、migrationファイルを手動で削除したりすると、file not found的なエラーがでます。どうやらautoloaderが認識してないようです。

```bash
composer dump-autoload
```
とするか、

```bash
php artisan optimize
```
とする。