Edited at

Laravelの標準Authentication(Auth)の動きを調べてみる

私は「あるものは使う派」なので、Authをよく使います。とは言え、標準のControllerやViewを使うことはあまりありませんが、このLaravel5.2から、make:authもできるようになったこともあり、標準の認証機能の動きをおさらいしておこうと思います。


準備

Authを利用するためには認証ユーザーのテーブルやら、Viewやらいろいろ必要なので準備していきます。なお、Laravelをインストールし、.envで適切にデータベースが設定されていることを前提に進めます。


認証用のデータベースの準備

新たに定義する必要もないので標準でmigrateが用意されているusersテーブルを利用します。

migrateを実行するだけ。

php artisan migrate

Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

これによりusersテーブルと、パスワードリセット時に利用されるpassword_resetsテーブルが生成されます。


usersテーブル

usersテーブルは下記のような構造。

パスワードは暗号化します。

remember_tokenは、ログイン情報を記憶しておく際に保存するcookie情報の一部となります。

mysql> desc users;

+----------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| password | varchar(60) | NO | | NULL | |
| remember_token | varchar(100) | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+----------------+------------------+------+-----+---------+----------------+

パスワードリセットの際に利用されるテーブル。

tokenの情報がリセット用のURLに添付され、リセット者を特定します。URLのリンクの有効期限も管理しています。

続いて、password_resetsテーブル。

mysql> desc password_resets;

+------------+--------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+-------------------+-----------------------------+
| email | varchar(255) | NO | MUL | NULL | |
| token | varchar(255) | NO | MUL | NULL | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+------------+--------------+------+-----+-------------------+-----------------------------+


有効期限は、config/auth.phpのpasswordsの中でexpire => 60(60分)と定義されています。



Viewの準備

ログイン画面等はもちろん自分で作ってもいいのですが、ここではとりあえず自動生成されるものを利用します。

Auth画面は付いたり、消えたりしていましたが、5.2からはmake:authで生成できるようになりました。

php artisan make:auth

Created View: /Users/hoge/laravel/resources/views/auth/login.blade.php
Created View: /Users/hoge/laravel/resources/views/auth/register.blade.php
Created View: /Users/hoge/laravel/resources/views/auth/passwords/email.blade.php
Created View: /Users/hoge/laravel/resources/views/auth/passwords/reset.blade.php
Created View: /Users/hoge/laravel/resources/views/auth/emails/password.blade.php
Created View: /Users/hoge/laravel/resources/views/layouts/app.blade.php
Created View: /Users/hoge/laravel/resources/views/home.blade.php
Created View: /Users/hoge/laravel/resources/views/welcome.blade.php
Installed HomeController.
Updated Routes File.
Authentication scaffolding generated successfully!

View、HomeControllerの追加、route.php等の編集が行われたようです。


ルートの用意

make:authで下記のようなRouteが生成されました。

php artisan route:list

+--------+----------+-------------------------+------+-----------------------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+-------------------------+------+-----------------------------------------------------------------+------------+
| | GET|HEAD | / | | Closure | |
| | GET|HEAD | home | | App\Http\Controllers\HomeController@index | web,auth |
| | GET|HEAD | login | | App\Http\Controllers\Auth\AuthController@showLoginForm | web,guest |
| | POST | login | | App\Http\Controllers\Auth\AuthController@login | web,guest |
| | GET|HEAD | logout | | App\Http\Controllers\Auth\AuthController@logout | web |
| | POST | password/email | | App\Http\Controllers\Auth\PasswordController@sendResetLinkEmail | web,guest |
| | POST | password/reset | | App\Http\Controllers\Auth\PasswordController@reset | web,guest |
| | GET|HEAD | password/reset/{token?} | | App\Http\Controllers\Auth\PasswordController@showResetForm | web,guest |
| | GET|HEAD | register | | App\Http\Controllers\Auth\AuthController@showRegistrationForm | web,guest |
| | POST | register | | App\Http\Controllers\Auth\AuthController@register | web,guest |
+--------+----------+-------------------------+------+-----------------------------------------------------------------+------------+

これらは、route.php内のRoute::auth()により生成されているようです。

Route::group(['middleware' => 'web'], function () {

//これ
Route::auth();

Route::get('/home', 'HomeController@index');
});


パスワードリセット用のメール送信準備

パスワードリセットにメール送信が必要なので、.envおよびconfig/mail.phpを編集してメールが送信できるようにしておきます。

今回はgmailのSMTPを利用しました。

設定等はこちらを参考にしてください。

これで環境準備は完了です。


まずは標準の動きを見ている


/(Root)

まずは/にアクセスしてみます。

お馴染みの標準のLaravelページが認証用?に変わっていますね。

lara_auth

このページ自体は、

Route::get('/', function () {

return view('welcome');
});

という感じで、ルーティングされ、単にwelcome(view)が表示されているだけのようです。


/home

次に、TOPメニューからHomeをクリックしてみます。

どうやら、このページを閲覧するにはログインが必要なようで、/homeは表示されず、/loginにリダイレクトされました。

lara_auth

route.phpを見てみると、単に、

Route::group(['middleware' => 'web'], function () {

//
Route::auth();

//これ
Route::get('/home', 'HomeController@index');
});

という感じで、HomeControllerのindexに飛ばしているだけのようなので、ログインのチェック自体は、Controller側で行っているようです。

では、Controllerを見てみます。

class HomeController extends Controller

{
//コンストラクタ
public function __construct()
{
$this->middleware('auth');
}

public function index()
{
return view('home');
}
}

どうやら、コンストラクタでauthミドルウエアが適用されているようです。


ミドルウエアは一般にRouteかControllerのコンストラクタで設定します。


では、authミドルウエアは何をしているのでしょうか?

まず、authミドルウエア自体は、app\Http\Kernel.php内で、

    protected $routeMiddleware = [

'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

という感じでエリアスが定義されていて、本体は、\App\Http\Middleware\Authenticate::classのようです。

この中を覗いてみると、

    public function handle($request, Closure $next, $guard = null)

{
//もし、ログインしてなかったら
if (Auth::guard($guard)->guest()) {
 
if ($request->ajax() || $request->wantsJson()) {
return response('Unauthorized.', 401);
} else {
// /loginにリダイレクト
return redirect()->guest('login');
}
}

return $next($request);
}

Auth::guard()->guest()により、もし、guest(ログインしてない)ならloginにリダイレクトしろ!ということになっています。


/register

ログインする前に、そもそも登録が必要なのでユーザーを登録をします。右上のRegisterメニューをクリックします。

そうすると、/registerにリンクしていて、登録画面が表示されます。

これは、

GET|HEAD | register                |      | App\Http\Controllers\Auth\AuthController@showRegistrationForm

によりルーティングされ、AuthControllerのshowRegistrationFormで処理されています。

ということで、AuthControllerを覗いてみると、showRegistrationFormなるメソッドは存在していません。

どうやら、AuthenticatesAndRegistersUsersトレイトに記述されているようです。

というわけで、Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsersを見てみると、さらにトレイトが呼び出されていて、

<?php

namespace Illuminate\Foundation\Auth;

trait AuthenticatesAndRegistersUsers
{
use AuthenticatesUsers, RegistersUsers {
AuthenticatesUsers::redirectPath insteadof RegistersUsers
;
AuthenticatesUsers::getGuard insteadof RegistersUsers;
}
}


認証系のメソッドはほぼトレイトとして実装されており、その実体は、Illuminate\Foundation\Auth\以下に存在します。


showRegistrationFormは、RegistersUsersの中で、

    public function showRegistrationForm()

{
if (property_exists($this, 'registerView')) {
return view($this->registerView);
}

return view('auth.register');
}

と定義されています。

registerViewが定義されていればそこへ、定義されていなければauth.register(view)を表示するようになっています。

では、登録してみます。

lara_auth

このformの定義は、

<form class="form-horizontal" role="form" method="POST" action="{{ url('/register') }}">

となっていますので、クリックすると、

POST     | register                |      | App\Http\Controllers\Auth\AuthController@register

でルーティングされ、AuthControllerのregisterで処理されることになります。

この処理もトレイとなっており、本体は、RegistersUsersにあるようです。

    public function register(Request $request)

{
$validator = $this->validator($request->all());

if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}

Auth::guard($this->getGuard())->login($this->create($request->all()));

return redirect($this->redirectPath());
}

登録し、かつ、その後ログインをし、AuthControllerで設定されたリダイレクト先へリダイレクトするようになっています。

というわけで/にリダイレクトされているようです。

lara_auth

これは、AuthControllerの

    /**

* Where to redirect users after login / registration.
*
* @var string
*/

protected $redirectTo = '/';

で記述されているからのようです。


再び/homeへ

再びHomeをクリックしてみましょう。今度はログインしている趣旨のメッセージが表示されました。

ログインが完了している場合、return view('home')により、home(view)が表示されます。

lara_auth

ここでは一旦ログイアウトします。

lara_auth

ログアウトのリンクは

<a href="http://localhost:8000/logout"><i class="fa fa-btn fa-sign-out"></i>Logout</a>

となっていて、/logoutにリンクされています。

つまり、クリックは、

GET|HEAD | logout                  |      | App\Http\Controllers\Auth\AuthController@logout

でルーティングされ、AuthControllerのlogoutメソッドで処理されます。

logoutメソッドの本体は、AuthenticatesUsersにあり、

    public function logout()

{
Auth::guard($this->getGuard())->logout();

return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
}

となっています。redirectAfterLogoutが定義されていればそのページに。なければ/にリダイレクトされるようです。


/login

では、一旦ログイアウトしたので、再度ログインしてみます。

lara_auth

ログインリンクは、

GET|HEAD | login                   |      | App\Http\Controllers\Auth\AuthController@showLoginForm 

でルーティングされAuthControllerのshowLoginFormで処理されます。この本体は、AuthenticatesUsersにあり、

    public function showLoginForm()

{
$view = property_exists($this, 'loginView')
? $this->loginView : 'auth.authenticate';

if (view()->exists($view)) {
return view($view);
}

return view('auth.login');
}

となっています。

loginViewが定義されていればそこに、なければ、auth.loginが表示されます。

ログインformの定義は、

<form class="form-horizontal" role="form" method="POST" action="{{ url('/login') }}">

となっているため、ログインボタンが押されたら、

POST     | login                   |      | App\Http\Controllers\Auth\AuthController@login 

でルーティングされ、AuthControllerのloginメソッドで処理されます。

loginメソッドの本体は、AuthenticatsUsersにあり、

    public function login(Request $request)

{
//バリデーション
$this->validateLogin($request);

//スロットル取得
$throttles = $this->isUsingThrottlesLoginsTrait();
 
//もし、規定の回数を超えていたら
if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);

return $this->sendLockoutResponse($request);
}

//認証情報取得
$credentials = $this->getCredentials($request);

//認証
if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
return $this->handleUserWasAuthenticated($request, $throttles);
}

//失敗だったらスロットルをカウントアップ
if ($throttles && ! $lockedOut) {
$this->incrementLoginAttempts($request);
}

return $this->sendFailedLoginResponse($request);
}

基本、認証がOKならログイン処理(handleUserWasAuthenticated)が行われます。

が、その他にも、いわゆるスロットル処理が行われており、ログインの失敗を数え、一定の数を超えると、しばらくログインできないという処理が加わっています(標準では60秒間に5回エラー)。


スロットル処理についてはここでは振れません。別途、記事にします。

ThrottleはThrottlesLoginsで定義されています。



/reset

次にパスワードリセット処理を見てみます。

ログイン画面の[Forget your password?]をクリックします。

lara_auth

このリンクは/password/resetにリンクされているので、

GET|HEAD | password/reset/{token?} |      | App\Http\Controllers\Auth\PasswordController@showResetForm 

でルーティングされ、PasswordControllerのshowResetFormで処理されます。

本体は、ResetsPasswordsにあり、

    public function showResetForm(Request $request, $token = null)

{
if (is_null($token)) {
return $this->getEmail();
}

$email = $request->input('email');

if (property_exists($this, 'resetView')) {
return view($this->resetView)->with(compact('token', 'email'));
}

if (view()->exists('auth.passwords.reset')) {
return view('auth.passwords.reset')->with(compact('token', 'email'));
}

return view('auth.reset')->with(compact('token', 'email'));
}

もし、tokenが設定されてなければ(リセット処理の際)、getEmail()が実行され、

    public function getEmail()

{
return $this->showLinkRequestForm();
}

さらに、showLinkRequestForm()が実行され、

    public function showLinkRequestForm()

{
if (property_exists($this, 'linkRequestView')) {
return view($this->linkRequestView);
}

if (view()->exists('auth.passwords.email')) {
return view('auth.passwords.email');
}

return view('auth.password');
}

結果として、auth.passwords.email(view)が呼び出され、emailの入力画面が表示されます。

lara_auth

emailを入力し、[Send Pasword Rest Link]を押します。

このformの定義は、

<form class="form-horizontal" role="form" method="POST" action="{{ url('/password/email') }}">

となっているので、

POST     | password/email          |      | App\Http\Controllers\Auth\PasswordController@sendResetLinkEmail

でルーティングされ、PasswordControllerのsendResetLinkEmailで処理されます。

PasswordControllerの処理も全てトレイトで処理されていて、sendResetLinkEmailは、ResetsPasswordsで定義されています。

    public function sendResetLinkEmail(Request $request)

{
   //バリデーション
$this->validate($request, ['email' => 'required|email']);

//ブローカーの生成
$broker = $this->getBroker();

//メール送信
$response = Password::broker($broker)->sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
});

switch ($response) {

//成功
case Password::RESET_LINK_SENT:
return $this->getSendResetLinkEmailSuccessResponse($response);
//失敗
case Password::INVALID_USER:
default:
return $this->getSendResetLinkEmailFailureResponse($response);
}
}

ここでPassword::broker()でメールを送っているようですね。

送信に成功したら、

return $this->getSendResetLinkEmailSuccessResponse($response);

失敗したら、

return $this->getSendResetLinkEmailFailureResponse($response);

が返されます。

上記メソッドは同じTrait内で定義されているので詳細はそれを見るといいでしょう。

とりあえず、成功したら下記のような画面が表示されます。

lara_auth

すると下記のようなメールが届きます。

lara_auth


なお、このメールの内容はresources/views/auth/emails/password.blad.phpを変更することでカスタマイズすることができるようです。


URLからわかる通り、これをクリックすると、

GET|HEAD | password/reset/{token?} |      | App\Http\Controllers\Auth\PasswordController@showResetForm

でルーティングされ、PasswordControllerのshowResetFormメソッドで処理されますが、今度はtokenがあるパターンです。

    public function showResetForm(Request $request, $token = null)

{
//tokenがなければ(なのでここでは処理されない)
if (is_null($token)) {
return $this->getEmail();
}

//ここでは以下の処理が実行される

//メール取得
$email = $request->input('email');

if (property_exists($this, 'resetView')) {
return view($this->resetView)->with(compact('token', 'email'));
}

//ここではこの処理が実行される
if (view()->exists('auth.passwords.reset')) {
return view('auth.passwords.reset')->with(compact('token', 'email'));
}

return view('auth.reset')->with(compact('token', 'email'));
}

今度はtokenがあるので、auth.passwords.resetが表示されることになります。

ここではemailが取得され、デフォルト値として設定されます。

lara_auth

ここのformの定義は、

<form class="form-horizontal" role="form" method="POST" action="{{ url('/password/reset') }}">

となっているため処理は、

POST     | password/reset          |      | App\Http\Controllers\Auth\PasswordController@reset

でルーティングされ、PasswordControllerのresetで処理されます。

    public function reset(Request $request)

{
$this->validate($request, [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:6',
]);

$credentials = $request->only(
'email', 'password', 'password_confirmation', 'token'
);

$broker = $this->getBroker();

//実際のリセット処理が行われている場所
$response = Password::broker($broker)->reset($credentials, function ($user, $password) {
$this->resetPassword($user, $password);
});
 
//結果の判断
switch ($response) {

//成功
case Password::PASSWORD_RESET:
return $this->getResetSuccessResponse($response);

//失敗
default:
return $this->getResetFailureResponse($request, $response);
}
}

正常に処理が完了するとhomeにリダイレクトされます。

lara_auth

ここまででリセット処理は完了です。

もっとTrait内のコードを丁寧に見たほうがいいと思いますが、流れを把握するという意味では以上とします。


パスワードの保存

先ほどは触れなかったパスワードの保存についても流れだけ見ておきます。

パスワードを記憶するには、ログイン画面にて[Remember me]にチェックを入れます。

このチェックボックスは、

<input type="checkbox" name="remember">

となっていますので、ログインの処理においてrememberパラメーターをどう処理するかがポイントです。

lara_auth

処理自体は、ログイン時と同じです。

AuthenticatesUsersの中のpostにいて、

        if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {

return $this->handleUserWasAuthenticated($request, $throttles);
}

としており、attempt()の第2引数として、$request->has('remember')を渡すことでパスワードを記憶します。

実際の記憶は、第2引数がtrueの場合、remember_xxxxというcookieが発行されます。この値は、ユーザーのprimary key(id) + | + remember_tokenとなるようです。なお、cookieは標準で暗号化されているので、見ただけで偽造することはできません。


remember_token()はログアウト処理時に生成されるように見えます。


パスワードを記憶すると、一度ブラウザを落として、再度、ログインが必要なページにアクセスしても、ログイン無く、ログインできます。


ログアウトした場合は、その限りではありません。


lara_auth

以上です。