ユースケース
ユーザーのモデルが複数あり、それぞれ別テーブルで管理されている場合のログイン処理
▼システムの利用ユーザー
管理者:Adminモデル、adminテーブル
教師:Teacherモデル、teacherテーブル
親:Parentモデル、parentテーブル
▼各ユーザーのIDフォーマット
管理者:6桁の数字+A (ex. 123456A)
教師:6桁の数字+T (ex. 123456T)
親:6桁の数字+P (ex. 123456P)
末尾に英文字を付与することでIDが重複することはない。
以上の要件で、同じログイン画面から、入力されたIDの末尾を元にモデルを判別し、
ログイン処理を行い、各ユーザーが使用する画面へと遷移させる処理を実装する。
バージョンはLaravel5.2です。
下準備
AuthControllerを実装。
マルチ認証で検索すると、各モデルごとに認証のコントローラーを作成する例がいくつか出てくるが、
1つでも十分可能なので1つにまとめる。
▼まずはログイン後にリダイレクトするURLを定義する。
protected $admin_redirectTo = '/admin/home';
protected $teacher_redirectTo = '/teacher/home';
protected $parent_redirectTo = '/parent/home';
▼ビジネスロジックはサービス層の別クラスに書くため以下も定義しておく。
protected $authService;
public function __construct(AuthService $authService) {
$this->authService = $authService;
}
▼ログイン画面のルーティング
Route::get('/login', 'AuthController@getLogin')->name('login');
Route::post('/login', 'AuthController@postLogin')->name('login');
▼コントローラーの処理
public function getLogin() {
return view('auth.login');
}
public function postLogin(Request $req) {
$isAuthenticated = $this->authService->authorize($req->all());
if($isAuthenticated) {
$role = $this->authService->getUserRole($req->input('user_id'));
switch($role) {
case 'admins':
return redirect($this->admin_redirectTo);
case 'teachers':
return redirect($this->teacher_redirectTo);
case 'parents':
return redirect($this->parent_redirectTo);
}
} else {
return redirect()->back()->with('error_msg', config('errors.auth_failed'));
}
}
IDとパスワードが一致しているかをAuthService#authorize()
でチェックする。
認証できればtrueが返ってくる。
そしてgetUserRole()
で入力されたIDの末尾からユーザーのタイプを判別して
それぞれの画面へとリダイレクトさせる。
public function getUserRole($user_id) {
if(ends_with($user_id, 'A')) {
return 'admins';
} else if(ends_with($user_id, 'T')) {
return 'teachers';
} else if(ends_with($user_id, 'P')) {
return 'parents';
}
return 'unknown';
}
$roleの値にsが付いてるのは後々のためです。
単数形の方がしっくりくる気がしますけどね。
認証処理authorize()の中身
入力されたユーザーIDから、ユーザータイプを判別、
それぞれのタイプごとの認証処理を行わせる。
public function authorize($data) {
$user_id = $data['user_id'];
$role = self::getUserRole($user_id);
$password = $data['password'];
if(Auth::guard($role)->attempt(['id' => $user_id, 'password' => $password, 'is_deleted' => 0])) {
$user = Auth::guard($role)->user();
session()->put('user_id', $user->id);
session()->put('user_name', $user->name);
session()->put('user_role', $role);
return true;
}
return false;
}
・ユーザーIDからレコードを検索
・パスワードが合ってるか確かめる
・削除済みでないか確かめる(is_deletedカラムの0,1で判定)
・合ってたらセッションにユーザーIDなどを保存
・認証が成功した証拠にtrueを返す
といった流れ。
レコードをどのテーブルから検索するかを指定しているのがAuth::guard()
の引数。
Auth::guard()とは
guardとは。
config\auth.phpを見てください。
デフォルトでは「セッションストレージとUser Eloquentモデルを使って認証を行うよ」というルールが定義されています。
要はここに適当な名前を付けて追加でルールを定義してやるだけ。
Auth::guard()
の引数にその名前を与えればそのルールで認証処理を行ってくれるわけです。
'guards' => [
'admins' => [
'driver' => 'session',
'provider' => 'admins',
],
'teachers' => [
'driver' => 'session',
'provider' => 'teachers',
],
'parents' => [
'driver' => 'session',
'provider' => 'parents',
],
],
どのモデルを使うかは別途prodviderに定義します。
'providers' => [
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
'teachers' => [
'driver' => 'eloquent',
'model' => App\Models\Teacher::class,
],
'parents' => [
'driver' => 'eloquent',
'model' => App\Models\Parent::class,
],
],
なんかguardの名前にはsをつけるのが通例のようです。さっきのgetUserRole
でsをつけたのはこのためです。
これでログイン処理ができました。
Auth::guard('teachers')
と指定するとTeacherクラス(teacherテーブル)からユーザーを検索する認証処理が実行されます。
モデルを認証用のモデルとして作成する。
Eloquentモデルなら何でもかんでも認証に使えるわけではありません。
以下のAdminモデルでは先ほどのAuth::guard('admins')
はエラーになります。
use Illuminate\Database\Eloquent\Model;
class Admin extends Model {
}
認証に使うモデルはIlluminate\Foundation\Auth\User
を継承する必要があるそうです。
デフォルトで用意されているUserクラスの形をそのままコピペしましょう。
use Illuminate\Foundation\Auth\User as Authenticatable;
class Admin extends Authenticatable
{
}
ちなみに今回のようにプライマリーキーに整数型の自動採番を用いない場合は、
モデルクラスにpublic $incrementing = false;
を記述する必要があります。
自動採番だとIDが重複する可能性があるため、今回の実装ではテーブルの特定ができません。
複数のモデルで同じIDが存在してしまう場合は、テーブルを特定(指定)するためにログインページを複数作る必要があります。
今回の例はIDの末尾でモデルが判断できるため1つのログインページで事足ります。
ユーザーごとにアクセス制限をかける
認証ができたのであとはルーティングです。
管理者が使う画面には当然他のタイプのユーザーがアクセスできないようにしなければなりません。
画面にアクセスしたときに権限を見て403画面を表示するでもいいのですが、
すっきりさせたいので全てroutes.phpで制御します。
権限がない画面だった場合はデフォルトのホーム画面へとリダイレクトさせます。
先のログイン認証後にリダイレクトしたページと同じです。
// 管理者のみアクセスできる
Route::group(['middleware' => ['auth:admins']], function() {
Route::get('/', function () {
return redirect()->route('admin_home');
});
// 管理者一覧
Route::get('/admin/index', 'AdminController@index')->name('admin_home');
// 管理者検索
Route::get('/admin/search', 'AdminController@search');
// 管理者詳細
Route::get('/admin/detail/{id}', 'AdminController@detail');
});
// 教師のみアクセスできる
Route::group(['middleware' => ['auth:teachers']], function() {
Route::get('/', function () {
return redirect()->route('teacher_home');
});
// 生徒一覧
Route::get('/student/info', 'StudentController@getHomeInfo')->name('teacher_home');
// 生徒検索
Route::get('/student/search', 'StudentController@search');
// 生徒詳細
Route::get('/student/detail/{id}', 'StudentController@detail');
});
Route::group(['middleware' => '['auth:guard']])
で囲むと、そのモデルで認証済みでなければ
指定されたURLにリダイレクトされます。ではURLはどこで指定しているんでしょうか?
上記の例で言えば、管理者でログインした状態で生徒一覧の画面にアクセスした場合は、
どこにリダイレクトされるの?
認証済みでない場合の処理を記述するのがMiddleware\Authenticate
Kernel.phpを見てもらえばわかるのですが、デフォルトの認証ミドルウェアであるauth
とひもづいているのがこの
\App\Http\Middleware\Authenticate.php
になります。
ルーティングでミドルウェアにauth
を指定すると先にこのクラスの処理が走ります。
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->guest()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
$user_id = session('user_id');
if(is_null($user_id)) {
return redirect()->guest('login');
} else {
if(ends_with($user_id, 'A')) {
return redirect()->route('admin_home');
} else if(ends_with($user_id, 'T')) {
return redirect()->route('teacher_home');
} else if(ends_with($user_id, 'P')) {
return redirect()->route('parent_home');
}
}
}
}
}
Auth::guard($guard)->guest()
がtrueの場合はつまり、指定されたguardで認証済みでない場合です。
Ajaxはまあ無視するとして、ここでもセッションに保存したユーザーIDを使います。
末尾からログイン中のユーザ―タイプを識別し、それぞれのホーム画面へとリダイレクトさせています。
そもそもログインしていない場合はセッションにユーザーIDが存在しないのでログイン画面へと飛ばしています。
さて、逆のパターンもやらなきゃです。
ログインしてたらアクセスできない画面を設定します。
ログイン画面とかですね。
認証済みの場合の処理を記述するのがMiddleware\RedirectIfAuthenticated
クラス名からも分かるように「認証済みであればここにリダイレクトしますよ」の設定をするクラス。
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
switch($guard) {
case 'admins':
return redirect()->route('admin_home');
case 'teachers':
return redirect()->route('teacher_home');
case 'parents':
return redirect()->route('parent_home');
}
}
return $next($request);
}
さっきより楽ですね。guardが分かっているので、セッションの値を見るなどのチェックが不要になります。
さて、このクラスはどうやったら呼ばれるのか。
Route::group(['middleware' => ['guest:admins, teachers, parents']], function() {
Route::get('/', function () {
return route('login');
});
// ログイン
Route::get('/login', 'AuthController@getLogin')->name('login');
Route::post('/login', 'AuthController@postLogin')->name('login');
});
ミドルウェアにguest
を指定します。
肝は適用するguardの種類を全て指定すること。
上記の例だとadminでログインしていてもteacherでログインしていてもparentでログインしていても
ログイン画面にはアクセスできないようにしています。
アクセスしようとするとRedirectIfAuthenticated
で定義したホーム画面へと飛ばされます。
複数ユーザーのログインを1つのシステム内でなるべく簡潔に済ませたかったので こんな感じになりました。この手のやり方はあまり情報がなくほとんど自前で考えながら書いたので もっといいやり方があるかもしれません。
以上です。
ご査収ください。