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


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

##準備

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

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

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

```bash
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情報の一部となります。

```bash
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テーブル。

```bash
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で生成できるようになりました。

```bash
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が生成されました。

```bash
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()により生成されているようです。

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

	//これ
    Route::auth();

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

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

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

設定等は[こちら](http://qiita.com/zaburo/items/37f28f0b621cbac74d15)を参考にしてください。

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

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

###/(Root)

まずは/にアクセスしてみます。
お馴染みの標準のLaravelページが認証用？に変わっていますね。

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/001.png)

このページ自体は、

```php
Route::get('/', function () {
    return view('welcome');
});
```

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

###/home

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

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/002.png)

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

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

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

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

```php
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内で、

```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のようです。
この中を覗いてみると、

```php
    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にリンクしていて、登録画面が表示されます。

これは、

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

によりルーティングされ、AuthControllerのshowRegistrationFormで処理されています。
ということで、AuthControllerを覗いてみると、showRegistrationFormなるメソッドは存在していません。
どうやら、AuthenticatesAndRegistersUsersトレイトに記述されているようです。
というわけで、Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsersを見てみると、さらにトレイトが呼び出されていて、

```php
<?php

namespace Illuminate\Foundation\Auth;

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

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

showRegistrationFormは、RegistersUsersの中で、

```php
    public function showRegistrationForm()
    {
        if (property_exists($this, 'registerView')) {
            return view($this->registerView);
        }

        return view('auth.register');
    }
```

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

では、登録してみます。

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/003.png)

このformの定義は、

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

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

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

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

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

```php
    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](http://www.bluecode.jp/qiita/lara_auth/004.png)


これは、AuthControllerの

```php
    /**
     * Where to redirect users after login / registration.
     *
     * @var string
     */
    protected $redirectTo = '/';
```

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

###再び/homeへ

再びHomeをクリックしてみましょう。今度はログインしている趣旨のメッセージが表示されました。
ログインが完了している場合、return view('home')により、home(view)が表示されます。

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/005.png)

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

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/006.png)

ログアウトのリンクは

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

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

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

でルーティングされ、AuthControllerのlogoutメソッドで処理されます。
logoutメソッドの本体は、AuthenticatesUsersにあり、

```php
    public function logout()
    {
        Auth::guard($this->getGuard())->logout();

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

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

###/login

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

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/007.png)


ログインリンクは、

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

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

```php
    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の定義は、

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

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

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

でルーティングされ、AuthControllerのloginメソッドで処理されます。
loginメソッドの本体は、AuthenticatsUsersにあり、

```php
    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](http://www.bluecode.jp/qiita/lara_auth/009.png)

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

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

でルーティングされ、PasswordControllerのshowResetFormで処理されます。
本体は、ResetsPasswordsにあり、

```php
    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()が実行され、

```php
    public function getEmail()
    {
        return $this->showLinkRequestForm();
    }
```

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

```php
    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](http://www.bluecode.jp/qiita/lara_auth/010.png)

emailを入力し、[Send Pasword Rest Link]を押します。
このformの定義は、

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

となっているので、

```bash
POST     | password/email          |      | App\Http\Controllers\Auth\PasswordController@sendResetLinkEmail
```
でルーティングされ、PasswordControllerのsendResetLinkEmailで処理されます。
PasswordControllerの処理も全てトレイトで処理されていて、sendResetLinkEmailは、ResetsPasswordsで定義されています。

```php
    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()でメールを送っているようですね。

送信に成功したら、

```php
return $this->getSendResetLinkEmailSuccessResponse($response);
```

失敗したら、

```php
return $this->getSendResetLinkEmailFailureResponse($response);
```

が返されます。
上記メソッドは同じTrait内で定義されているので詳細はそれを見るといいでしょう。
とりあえず、成功したら下記のような画面が表示されます。

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/011.png)

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

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/012.png)

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

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

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

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

```php
    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](http://www.bluecode.jp/qiita/lara_auth/014.png)

ここのformの定義は、

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

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

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

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

```php
    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](http://www.bluecode.jp/qiita/lara_auth/015.png)

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

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

##パスワードの保存

先ほどは触れなかったパスワードの保存についても流れだけ見ておきます。
パスワードを記憶するには、ログイン画面にて[Remember me]にチェックを入れます。

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

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

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

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/016.png)

処理自体は、ログイン時と同じです。
AuthenticatesUsersの中のpostにいて、

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

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

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

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


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

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

![lara_auth](http://www.bluecode.jp/qiita/lara_auth/017.png)

以上です。