$artisan make:auth
したという前提で。
Laravel5.4でマルチ認証(userとadmin)を実装する方法 | 大分のITコンサルタント | 高橋商店
というのをやってみて、認証の中で振り分けをしたかったというケース。
普通にファーストアクセスで認証必要なURLにアクセスした場合
URL→routes→Controllerなので…
最初はController
認証を掛けたいController内でAuthを呼ぶ。
<?php
class UserController extends Controller
{
@return
public function __construct()
{
$this->middleware('auth:user,user1,user2,user3');
}
ってやると、MiddleWareで登録されたauthが起動する。
app/Http/Kernel.phpで
app/Http/Kernel.php
<?php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
のauthにあるauthとさっきの$this->middleware('authのauthが対応している。auth:user, user1,user2,user3のuser1,user2,user3はMiddleWare(この場合Authenticate)の$guards引数になる。
ちなみに、$routeMiddleWareというのは$middleWareと違ってControllerでこうやって$this->middleware()して任意に起動させるもので、$middleWareは全てのrouteで起動する。
Authenticate.php
で、この起動するインスタンスは
app/Http/Middleware/Authenticate.php
なので、このインスタンスのhandleメソッドが実行される。
これは、Illuminate\Auth\Middleware\Authenticateを継承しているので、
Illuminate\Auth\Middleware\Authenticate
<?php
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
がメソッド。で、この$guardsがさっきの$this->middleware('auth:user,user1,user2,user3')した引数になってて、
<?php
$guards = ['user', 'user1', 'user2', 'user3', ];
となっている。
Illuminate\Auth\Middleware\Authenticate
この $guardsはIlluminate\Auth\Middleware\Authenticate->authenticate()で使われて、
Illuminate\Auth\Middleware\Authenticate
<?php
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
となっている。このforeach($guardsに $guards = ['user', 'user1', 'user2', 'user3', ]; が入ってくる。この場合4つのGuardsを使うことになる。
で、この$this->authは Illuminate/Auth/AuthManager.php のインスタンス。
Illuminate\Auth\AuthManager と config/auth.php
ここで、check()メソッドを持つインスタンスをconfig/auth.phpに基づいて返す。
auth.phpでは、'guards' => に入っているもので
config/auth.php
<?php
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'user' => [
'driver' => 'session',
'provider' => 'custom_users',
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
となっているので、$this->middleware('auth:user')とすると、driverにsession/providerにcustom_usersを使ったものがcheck()を実行する。
ちなみに、$this->middleware('auth')だと、defaultsのguardは'web'なのでこれまたdriverにsession/providerにusersを使ったものが実行される。
AuthのProviderは、Modelとつながっている
と、ここまでで、普通にControllerで$this->middleware('auth')としたときにはguardsのwebが呼ばれて、そのコードはproviderで示されるusersなのである。
で、Illuminate\Auth\Middleware\Authenticate::authenticate()の
Illuminate\Auth\Middleware\Authenticate::authenticate()
<?php
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
では、$this->authがIlluminate\Auth\AuthManagerなので、そのguardメソッドを見るとAuthManager::resolve()で$guardに沿ったインスタンスを作っている。
'web'の場合、driverがsessionなのでAuthManager::createSessionDriver()である。これは$this->auth->guard($guard) がIlluminate\Auth\SessionGuardを作っている。
このSessionGuardインスタンスは、以下の経緯で作られる$providerを持っている。
Illuminate\Auth\CreatesUserProviders::createUserProvider($provider)
で、$providerがeloquentであり、特に$providerに対するカスタムなCreatorが無い
↓
Illuminate\Auth\CreatesUserProviders::createEloquentProvider($config)
これはEloquentUserProviderインスタンスをnewする。
↓
new EloquentUserProvider($this->app['hash'], $config['model']);
また、SessionGuardインスタンスは、識別子$name(この場合は'user')と$sessionと(Http)Request $requestを持っている。
Illuminate\Auth\SessionGuard のcheck()
は、Illuminate\Auth\GuardHelpers::check()のtraitで、SessionGuard::user()の返り値がNULLでないかをチェックしている。
このuser()メソッドは
- ログアウトのタイミングならnull
- SessionGuard::$user (Illuminate\Contracts\Auth\Authenticatable) があればそれを返す。これは既にログイン処理がされている場合にはSessionからRestoreされるっぽい。
とかまぁなんかそんな感じ。
ここまでで、このSessionGuard::$userが得られれば認証通過。
ログインが認可されているセッションCOOKIEを持っている場合
SessionGuard::user()で$this->userを作成する処理を行う。
Illuminate\Auth\SessionGuard::user()
<?php
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
の部分。以降では逐一DBにアクセスする必要は無くなる。
「永続ログイン」のチェックを入れてログインしていると、
Illuminate\Auth\SessionGuard::user()
<?php
if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
$this->user = $this->userFromRecaller($recaller);
の部分で$this->userの復帰をしてくれる。
LaravelはSessionを自前で実装している。
…とここまで読んで、LaravelはPHPの生のセッション機能を使ってなくて独自でSession構築しているということに気が付いた。えー。秘密情報をCOOKIEに入れてるのかな?…あれ?これ以前も「すげー」って思った記憶あるな…いや、それはRailsだったっけ?Laravelのデフォルトだとセッション情報はファイルで storage/framework/sessions に保存してる。
ログインの時の処理。
DBに接続する処理があるのはここの部分だけですね。
Auth\LoginController@login
メソッドとしてはこのメソッドなので
Illuminate\Foundation\Auth\AuthenticatesUsers::login()を見に行く。
Illuminate\Foundation\Auth\AuthenticatesUsers
<?php
public function login(Request $request)
{
$this->validateLogin($request);
if (method_exists($this, 'hasTooManyLoginAttempts') &&
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
validateLogin()
ここで、ユーザー名とパスワードの入力チェック。独自のチェックを入れたかったら、ControllerでOverrideすればいい。
attemptLogin()
hasTooManyLoginAttempts()は、多重ログインPOSTの防止かな。あまり読んでない。
ログイン処理のメインはattemptLogin()で
Illuminate\Foundation\Auth\AuthenticatesUsers
<?php
protected function attemptLogin(Request $request)
{
return $this->guard()->attempt(
$this->credentials($request), $request->filled('remember')
);
}
で、guard()で得たインスタンス(SessionGuard)のattempt()を実行する。これは
Illuminate\Auth\SessionGuard
<?php
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
if ($this->hasValidCredentials($user, $credentials)) {
$this->login($user, $remember);
return true;
}
DBにつなぐところは、 $this->provider->retrieveByCredentials()の所。このproviderはlluminate\Auth\EloquentUserProviderである。このretrieveByCredentialsメソッドが
lluminate\Auth\EloquentUserProvider
<?php
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('password', $credentials))) {
return;
}
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'password')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} else {
$query->where($key, $value);
}
}
return $query->first();
}
となっている。newModelQuery()でQueryを取っているが、このORMはauth.phpに記述した 'providers' => ['users' => [ のmodelのApp\User::classとなる。
ちなみにこの定義が無いとデフォルトのグローバルの\Illuminate\Database\Eloquent\Builderがnewされて作られる。…が、ほとんど意味が無いだろう。テーブル持って無いし。ちゃんと定義しろということですね。これで、メールアドレスを指定でDBから1行を抜き出す。
で、その後は更にhasValidCredentials()に流れて、
Illuminate\Auth\SessionGuard
<?php
protected function hasValidCredentials($user, $credentials)
{
return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
}
で、この$this->providerはIlluminate\Auth\EloquentUserProviderのインスタンスで、validateCredentialsでパスワードと比較している。
Illuminate\Auth\EloquentUserProvider
<?php
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
ちなみに、この時UserNameとして何を使いたいかというと、Illuminate\Foundation\Auth\AuthenticatesUsersのusername()メソッドをControllerで上書きすればよい。デフォルトだと以下のようにemailが使われる。
Illuminate\Foundation\Auth\AuthenticatesUsers
<?php
public function username()
{
return 'email';
}
ログインに失敗した時
単純に権限のない所に入ってしまった場合
これは、Illuminate\Auth\Middleware\Authenticate->authenticate()で例外AuthenticationExceptionを投げられる。
これは、app\Exceptions\Handler::unauthenticated() でキャッチされる。
app\Exceptions\Handler
<?php
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
if (in_array('admin', $exception->guards())) {
return redirect()->guest('admin/login');
}
return redirect()->guest('login');
}
エラーハンドリング
で、なんでAuthenticationException エラーの時だけunauthenticatedメソッドが使われるかというと、最終的にはapp\Exceptions\Handler::render()でキャッチされるのだけど、
Illuminate\Foundation\Exceptions\Handlerで、
Illuminate\Foundation\Exceptions\Handler
<?php
public function render($request, Exception $e)
{
if (method_exists($e, 'render') && $response = $e->render($request)) {
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
return $e->toResponse($request);
}
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}
となっていて、AuthenticationExceptionの例外の場合はunauthenticated()メソッドが実行されるため。
権限のないところに入ったら、所定のログイン場所に飛ばしたい
投げられる認証の例外AuthenticationExceptionのnewはapp\Http\Middleware\Authenticateの継承元である
Illuminate\Auth\Middleware\Authenticate
<?php
protected function authenticate($request, array $guards)
{
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
なので、$this->redirectTo($request)がキーになるので、app\Http\Middleware\AuthenticateのredirectTo先を変更してやると良い。
…が、このMiddlewareの$thisは自分がadminなのかuserなのかが分からないので、どこにredirectするべきかが分からない…
というのは、
Illuminate\Auth\Middleware\Authenticate
<?php
protected function authenticate($request, array $guards)
{
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
とnewしているところで$guardsを渡してくれてない…
ということで、Http\Middleware\Authenticateにguardsを渡してその先頭を取る
どのguardsで引っかかったかというのはコアのAuthenticateでは取れないので、
app\Http\Middleware\Authenticate
<?php
@var
private $guards ;
public function handle($request, Closure $next, ...$guards)
{
$this->guards = $guards;
として、handle時に持たせてやり、configのauthにredirectoToを生やして
config/auth.php
<?php
'guards' => [
'user' => [
'driver' => 'session',
'provider' => 'users',
'redirectTo' => 'user.login'
],
redirectoToで使用した。
app\Http\Middleware\Authenticate
<?php
protected function redirectTo($request)
{
$redirectTo = sprintf('auth.guards.%s.redirectTo', current($this->guards));
$redirectTo = \Config::get($redirectTo) ?? '';
if (!$request->expectsJson()) {
return route($redirectTo);
}
}
んー。まぁ、こんな感じで。
Auth::routes()でのRouteの追加
routes/web.phpに Auth::routes(); を記載すると、Illuminate\Routing\Router->auth()が実行される。
Illuminate\Routing\Router
<?php
public function auth(array $options = [])
{
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
$this->post('logout', 'Auth\LoginController@logout')->name('logout');
if ($options['register'] ?? true) {
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');
}
if ($options['reset'] ?? true) {
$this->resetPassword();
}
if ($options['verify'] ?? false) {
$this->emailVerification();
}
}
となっているので、手動で追加したい場合はこのrouteを記載する。
Login時のSessionGuardをうまく働かせる
ログインした状態で認証をパスする場合、Illuminate\Auth\SessionGuard->user()でセッションを見るんだけど、この場合
$this->middleware('auth:user')
で保存しなければならない。チェックする時が「auth:user」でなので。これを指定せずにいると、デフォルトのwebが使われてセッションに保存されてしまって認証状態ではないと判断してしまう。
SessionGuard::login() でSessionにデータを保存する処理を追ってみる
結局、SessionGuardをnewする所で$nameを与えているので、
Illuminate\Foundation\Auth\AuthenticatesUsers.php
<?php
protected function guard()
{
return Auth::guard();
}
ここで$nameを与えてないので仕方ない…
ということで、このAuth::gurad($name)っていう感じのClassを作ってOverrideする。
app\Http\Controllers\Auth\LoginController
<?php
class LoginController extends Controller
{
use AuthenticatesUsers;
protected $guard_name = 'user';
とControllerで$nameを指定できるようにしオリジナルのを使わないようにして、自前のAuthenticatesUsersを作る。
で、同じディレクトリに
app\Http\Controllers\Auth\AuthenticatesUsers
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers as AuthenticatesUsersOrig;
use Illuminate\Support\Facades\Auth;
trait AuthenticatesUsers
{
use AuthenticatesUsersOrig;
protected function guard()
{
return Auth::guard($this->guard_name);
}
}
とやっておく。