Laravelには便利な認証機構が存在しますが、標準ではusersテーブルのみを使ってユーザーを認証します。
実際の開発ではadministratorsとusersを分けたいことはよくあることなので、テーブルを分けた認証をしてみます。
Laravel4の時代は定番のパッケージなどがあったようですが、Laravel5には対応していないようで、仕方ありません。
###追記
5.1対応のパッケージを見つけたので、その使い方を書きました。こちらの方がオススメです。
###参考
ちょうど、目的とする内容の記事を見つけたので参考にさせていただきました。
###やりたいこと
・認証用のユーザーをusersとadministratorsでテーブルを分けて、それぞれ認証する。
・一般ユーザーは、標準のusersおよび標準ドライバを利用。
・userの認証はAuth::attempt(['email'=>$email,'password'=>$password])ですが、これに加え、
・adminの認証はAuth::driver('admin')->attempt(['email'=>$email,'password'=>$password])とできるようにする。
###やること
やることは、administratorsテーブルの構造をこらなければ、大きくは下記の2つ。
・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的な機能が必要な場合、
・1つのテーブルでroleを追加し、attempt()でroleも含めて認証する
・複数のテーブルで、identifierとなるカラムの値を重複させないように実装する
・Sessionの書き込み、読み込みを工夫しidentifierの重複を防ぐ(Guardを拡張?)
で対応かな。正直、あまり複雑になってくると、毎度おなじみフレームワークの意味が無いなあ。
###下準備
####administratorsテーブルの準備
migrationファイルを作成する。
php artisan make:migration create_administrators_table --table=administrators
などとする。
とりあえず、administrator_idは、stringにして、admin_プリフィック+uniqid()で生成することにする。なんか釈然としない。
database/migrations/XXXXX_create_administrators_table.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');
}
}
こんな感じ。
php artisan migrate
で、usersテーブルも一緒に作成されます。
####データの準備
users, administratorsに予めユーザーを登録しておきましょう。formを作ってもいいし、insertしてもいいですが、折角なのseederを利用します。
seederを作るartisanコメンドはないようなので、手書きします(なんで?)
なお、ここでは、Eloquent ORMを利用しているので、Administrator.php(モデル)を作成しておく。
いろいろ書いているが、基本、User.phpをコピーして、class名と$tableだけ変えればいいと思う。
app/Administrators.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という1人のユーザーを足しておく。とりあえずidはuniqid()で生成。
database/seeder/AdministratorTableSeeder
<?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は、
php artisan db:seed
で実行されるが、この時実行されるのは、DatabaseSeeder.phpのみである。そのため、DatabaseSeeder.phpにAdministratorTableSeeder.phpが実行されるように記述する。
DatabaseSeeder.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 namespace App;
use Illuminate\Auth\GenericUser;
class AdminUser extends GenericUser
{
public function getAuthIdentifier(){
return $this->attributes['administrator_id'];
}
}
####カスタムUserProviderの実装
app/AdministratorProvider.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 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')をつけて実行します。
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;
});
####ログアウト
Route::get('logout',function(){
Auth::driver('admin')->logout();
});
###Middlewareを定義してみる
折角なので、これらのドライバを利用するミドルウエアを記述してみます。/admin以下はadmin権限が必要という感じにしたい。
ひな形は
php artisan make:middleware AdminAuthenticate
で作れます。
<?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に登録します。
/**
* 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で、
Route::get('admin',['middleware'=>'admin',function(){
return "/admin";
}]);
Route::get('admin/tools',['middleware'=>'admin',function(){
return "/admin/tools";
}]);
などとします。ルートが多い場合はgroupを利用したほうがいいでしょう。下記は上記と同義。
Route::group(['prefix'=>'admin','middleware'=>'admin'],function(){
Route::get('/',function(){
return "/admin";
});
Route::get('tools',function(){
return "/admin/tools";
});
});
admin/*みたいなのはないんかね。
###メモ
全てのmigrationをやり直すには、
php artisan migration:refresh
とする。 --seedオプションを使うとseedもやりなおしてくれるという記述を見つけたけど、動きませんでした。
あと、migrationファイルを手動で削除したりすると、file not found的なエラーがでます。どうやらautoloaderが認識してないようです。
composer dump-autoload
とするか、
php artisan optimize
とする。