You are here
Home > Posts tagged "ユーザー認証" (Page 2)

ユーザー認証(13)Laravel 5.2 Hashを複数使用する

ユーザー認証(10)Laravel 5.2 マルチ認証では、会員と管理者に対して異なるDBテーブルをもとに認証を設定しました。

また、

前回では、違うHasherの使用を試みました。

今回は、マルチ認証のときに異なるHasherを用いるケースについて考えてみましょう。そうたくさん起こるケースでないかもしれませんが、私のクライアントのシステムでは実際に起こるケースです。1つのシステムにおいて、「会員」と「管理者」と「店舗管理者」が存在し、それぞれの認証は異なるHasherを使用しています。特に「管理者」は複数のシステムで共有するもので、その認証のためのサーバーが違うマシンに存在します。

まず直面する問題は、Laravelの5.2のマルチ認証では、このような状況にはシンプルに対応できないことです。

config/auth.phpprovidersでは、それぞれのproviderにおいてhasherの設定がないのです。

..
   'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        'admin_users' => [
            'driver' => 'eloquent',
            'model' => App\AdminUser::class,
        ],
    ],
..

また、前回のように、グローバルでHasherを変えることもできません。

「会員」の認証のときには、デフォルトのHasherを使用し、「管理者」の認証のときには、違うHasherを使用できるのが理想です。

まず、Authのサービスがどう初期化されているか追跡してみましょう。

AuthServiceProviderで、ユーザー認証のサービスauthが登録されます。そこでは、AuthManagerのクラスが使用されます。

  protected function registerAuthenticator()
    {
        $this->app->singleton('auth', function ($app) {
            $app['auth.loaded'] = true;
            return new AuthManager($app);
        });
        $this->app->singleton('auth.driver', function ($app) {
            return $app['auth']->guard();
        });
    }

AuthManagerでは、config/auth.phpの設定を読み込み、providerを作成します。その作成は、CreatesUserProvidersで行われます。

..
    public function createSessionDriver($name, $config)
    {
        $provider = $this->createUserProvider($config['provider']);
..

providerdriverは、先のconfig/auth.phpではeloquentと設定されているので、以下のメソッドでオブジェクトが作成されます。

..
   protected function createEloquentProvider($config)
    {
        return new EloquentUserProvider($this->app['hash'], $config['model']);
    }
..

やっとたどり着きましたね。そうproviderの作成時に、グローバルのHasher app['hash']がパラメとして渡されているのです。

ここを変えることができれば、認証のHasherを変えることできるのです。変更するには、新規の認証のためのdriverを使用することも可能です。しかし、以下のEloquentUserProviderを見ると、オブジェクトが作成された後でもHasherを変えることが可能のようです。

...
    /**
     * Sets the hasher implementation.
     *
     * @param  \Illuminate\Contracts\Hashing\Hasher  $hasher
     * @return $this
     */
    public function setHasher(HasherContract $hasher)
    {
        $this->hasher = $hasher;
        return $this;
    }
...

ここまで理解すると、あとはそう難しくはありません。

まずは、config/auth.phpにおいて、どのHasherのサービスを使用するか指定しましょう。

...
   'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
       'hasher' => Illuminate\Hashing\BcryptHasher::class
        ],

        'admin_users' => [
            'driver' => 'eloquent',
            'model' => App\AdminUser::class,
            'hasher' => App\Services\MD5Hasher::class
        ],
    ],
...

上のhasherはまったくLaravelのコードでは使用されませんが、今回の目的でHasherを指定するにはベストの場所と思いませんか!

次に、管理者のAuthControllerにおいて、

...
class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers, ThrottlesLogins {
        getCredentials as getCredentialsTrait;
    }

    protected $redirectTo = '/admin/home';

    protected $guard = 'admin';
    protected $redirectAfterLogout = 'admin/login'; //ログイン後のリダイレクト先
    protected $username = 'email'; // DBテーブルのログインに使用される項目

    protected $registerView = 'admin.auth.register'; // 登録に使用されるテンプレート
    protected $loginView = 'admin.auth.login'; // ログインに使用されるテンプレート

    protected $hasher;

    public function __construct()
    {
        $hasher_class = config('auth.providers.admin_users.hasher');

        $this->hasher = new $hasher_class;

        \Auth::guard($this->guard)->getProvider()->setHasher($this->hasher);

        $this->middleware('guest:admin', ['except' => 'logout']);
    }
...

会員と違うテンプレートを用意するために、テンプレートの場所を指定していることにも注意してください。すべて変数の指定で可能です。

PasswordControllerにおいても同様な設定をします。

class PasswordController extends Controller
{
    use ResetsPasswords;

    protected $redirectTo = '/admin/home'; // ログイン後のリダイレクト先
    protected $guard = 'admin';

    protected $linkRequestView = 'admin.auth.passwords.email'; // パスワードのリセットリンクを送信してもらう画面のテンプレート
    protected $resetView = 'admin.auth.passwords.reset'; // パスワードリセット画面のテンプレート
    protected $subject = '管理者のパスワードリセット'; // 送信メールの件名

    public function __construct()
    {
        $hasher_class = config('auth.providers.admin_users.hasher');

        \Auth::guard($this->guard)->getProvider()->setHasher(new $hasher_class);

        $this->middleware('guest:admin');
    }
}

最後に、管理者へのパスワードリセットのメールのテンプレートは、以下のように、config/auth.phpで可能です。

...
  'passwords' => [
        'users' => [
            'provider' => 'users',
            'email' => 'user.auth.emails.password',
            'table' => 'password_resets',
            'expire' => 60,
        ],

        'admin_users' => [
            'provider' => 'admin_users',
            'email' => 'admin.auth.emails.password',
            'table' => 'admin_password_resets',
            'expire' => 60,
        ],
    ],
];

ユーザー認証(12)Laravel 5.2 Hasherを変える

Hasherとは、パスワードからHashの作成に使用される関数です。さて、Hashとはなんぞや?

例えば、パスワードをtesttestとします。これをHasherに与えると、
$2y$10$CE4R5SS6f5g4Rd0fgYRbneoeCOYbE0S2xfaYNC7i41CLysQ8TRUPO
のような文字列を生成します。これがHashです。

Hashは暗号化と異なり、非暗号化はできる機能はありません。つまり、
$2y$10$CE4R5SS6f5g4Rd0fgYRbneoeCOYbE0S2xfaYNC7i41CLysQ8TRUPO
から、もとのパスワードtesttestを解読できる機能はありません。

この特性を活かしてユーザー認証の機能のセキュリティを高めます。

まず会員登録時に入力したパスワードをHashした値をDBに保存します。そのままのテキストでは保存しません。そして、ログイン時に入力されたパスワードをHashして、DBに保存されたHashされたパスワードとマッチするかチェックします。マッチするなら認証OKです。

このHashする関数(Hasher)にはいろいろな種類があります。PHPでは、一昔前までは、

md5

を使用していました。md5は、必ず同じ値を返すので、Hashされたパスワードのマッチは、お互いの文字列を比較するだけです。

しかし、php5.5からは、よりセキュアな以下の関数の使用が薦められています。

password_hash

この関数が返すHashの値は、与えられる値が同じでも返す値が変わります。それゆえに、Hashされたパスワードのマッチにはもう1つの関数、

password_verify

を使用します。以下のように。


$password = 'testtest';

$hashed = password_hash($password, PASSWORD_DEFAULT);

$result = password_verify($password, $hashed);

echo $result ? '認証成功' : '認証失敗';

Laravelも基本的に同様な関数を使用しています。

BcryptHasher

さて、新規にLaravelを使用してプログラムを書くならまったくこれで問題ありません。しかし、既存のプログラムをLaravelに書き換えるとき、例えば、既存のプログラムがパスワードのHashにmd5を使用しているなら、DBに保存されているパスワードでは、LaravelのHasherでは誰もログインが不可能となってしまいます。

これに対応するには、LaravelのHasherを取り換える必要があります。HasherはLaravelではプログラムを通して1種類しか使用できない仕組みなので、HasherのProviderを作成して取り換えることになります。

まず、新規のHasherを作成します。


namespace App\Services;

class MD5Hasher implements \Illuminate\Contracts\Hashing\Hasher {

    public function make($value, array $options = [])
    {
        return md5($value);
    }

    public function check($value, $hashedValue, array $options = [])
    {
        return (md5($value) == $hashedValue);
    }

    public function needsRehash($hashedValue, array $options = [])
    {
        return false;
    }
}

\Illuminate\Contracts\Hashing\Hasherで定められている型をもとに、必要な関数を定義します。

次にこのHasherを登録するサービスプロバイダーを作成します。


namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use App\Services\MD5Hasher;

class MD5HasherServiceProvider extends ServiceProvider {

    protected $defer = true;

    public function register()
    {
        $this->app->singleton('hash', function() {
            return new MD5Hasher;
        });
    }

    public function provides()
    {
        return ['hash'];
    }
}

そして、config/app.phpを編集して、Hasherをサービスプロバイダーを入れ替えます。

...
   'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        ...
        // Illuminate\Hashing\HashServiceProvider::class, //コメントして
        App\Providers\MD5HasherServiceProvider::class,   //登録する
...

最後に、以下をコマンドラインで実行します。

composer dump-autoload

ユーザー認証(11)Laravel 5.2 ログインの記録

「ユーザー認証」のポストは、もうすでに11回目になりました。Taylorくんのプログラムは、宝石がいっぱい詰まっているから、ソースコードを見ているといろいろ発見あります。

例えば、私はユーザー認証成功後にログインの記録が欲しいです。つまり、ユーザーがどのIPからどのブラウザあるいはどのOSでアクセスしたかをDBに記録したいのです。ソースコードを見ているとそのことをあたかも考慮しているメカニズムが存在することに気づきます。今回はそれをどう利用するかを紹介します。

まず、ユーザー認証(9)Laravel 5.2 コンポーネント自動作成で紹介したコマンドで作成されるファイルの1つ、AuthController.phpを編集することになります。しかし、以下を見てわかるようにトレイトをバンバン使っているので、そのファイルの中身はほとんど空です。

namespace App\Http\Controllers\Auth;

use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers, ThrottlesLogins;

    protected $redirectTo = '/';

    public function __construct()
    {
        $this->middleware('guest', ['except' => 'logout']);
    }

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|confirmed|min:6',
        ]);
    }

    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }
}

さて、どうすればよいでしょう?

上の11行目のトレイトのファイルを遡って辿ります。

AuthenticatesAndRegistersUsers

AuthenticatesUsers

このファイルで、postLoginは、ログイン情報を送信したときに呼ばれる関数、その定義の中では、loginをコールしています。

...
    public function postLogin(Request $request)
    {
        return $this->login($request);
    }
...
    public function login(Request $request)
    {
        $this->validate($request, [
            $this->loginUsername() => 'required', 'password' => 'required',
        ]);
 
        $throttles = $this->isUsingThrottlesLoginsTrait();

        if ($throttles && $this->hasTooManyLoginAttempts($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) {
            $this->incrementLoginAttempts($request);
        }
        return $this->sendFailedLoginResponse($request);
    }
...
    protected function handleUserWasAuthenticated(Request $request, $throttles)
    {
        if ($throttles) {
            $this->clearLoginAttempts($request);
        }
        if (method_exists($this, 'authenticated')) {
            return $this->authenticated($request, Auth::guard($this->getGuard())->user());
        }
        return redirect()->intended($this->redirectPath());
    }
...

loginの定義ではattempt()(21行目)で認証を実行しています。そしてそれが成功なら、handleUserWasAuthenticatedをコールします。

今度は、36行目見てください。ここのifは、このクラスにauthenticatedというメソッドが定義されているなら、それを実行しますよ!という意味です。

先のAuthController.phpに以下のメソッドを追加します。以下のハイライトされた
部分です。

namespace App\Http\Controllers\Auth;

use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

use App\UserLog;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers, ThrottlesLogins;

    protected $redirectTo = '/';

    public function __construct()
    {
        $this->middleware('guest', ['except' => 'logout']);
    }

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|confirmed|min:6',
        ]);
    }

    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }

    public function authenticated(Request $request, User $user)
    {
        UserLog::add($request, $user);//ここでログを作成

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

これで、ログインが成功するたびに、このauthenticatedがコールされログインの履歴をDBに作成します。

以下はそこで使用されるモデルUserLogです。ホスト名を得るために、ミューテーターを使用します。


namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;

class UserLog extends Model {

    protected $table = 'user_log';
    protected $primaryKey = null;
    public $incrementing = false;
    public $timestamps = true;

    public function setHostnameAttribute($ip)
    {
        $this->attributes['hostname'] = gethostbyaddr($ip);
    }

    public static function add(Request $request, User $user)
    {
        $userLog = new static;

        $userLog->user_id = $user->id;
        $userLog->ip = $request->getClientIp();
        $userLog->hostname = $userLog->ip; // ミューテーターを使用

        $userLog->save();
    }
}

user_logのDBテーブルを作成することをお忘れずに。

authenticatedには、ログインの履歴を作成するだけでなく、会員の1周年記念のためのお祝いのメッセージや、それにまつわる特典の提供とか、いろいろなコードを含むことが考えられます。

ユーザー認証(10)Laravel 5.2 マルチ認証

マルチ認証と言っても、複数のステップでユーザーを認証するわけでもなく、ちょっとピンと来ないですね。

例えばECシステムにおいて、ユーザー画面での会員ログインと、管理画面での管理者のログインがそれぞれ別に必要とします。どちらもログインはEメールとは限らないし、片方でログインしたらもう片方でも認証となるとも限りません。つまり、ログインするユーザーの種類や場所が複数必要となる状況が多々あります。それに対応する機能が、マルチ認証です。

Laravelの5.1までは、マルチ認証は対応していなく、以下のようなパッケージをインストールして使用していました。

Laravel4.2対応のLaravel Multi Auth

Laravel5.1対応のMultiAuth for Laravel 5.1

しかし、5.2からはLaravelの基本仕様となっています。さすが、Taylorくん!

今回はこの機能を見てみましょう。

まず、デフォルトでインストールされるconfig/auth.phpの中身の解析。

Laravel 5.1では、

return [

    /* デフォルトの認証ドライバー */

    'driver' => 'eloquent',

    /* 認証に使用されるモデル */

    'model' => App\User::class,

    /* 認証に使用されるDBテーブル。ここでは、dirverがdatabaseでないので関係ない */

    'table' => 'users',

    /* パスワードリセットの設定 */
    'password' => [
        'email'  => 'emails.password', // resources/views/emails/passwordをリンク送信メールのテンプレートとする
        'table'  => 'password_resets',  // パスワードリセットのトークンの情報を保存するDBテーブル
        'expire' => 60,                 // トークンは60分で期限切れ
    ],
];

これがLaravel5.2では、

return [
    /* 認証のデフォルト設定 */

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],                         

    /* 認証のガードを定義 */                                                   

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],
    ],

    /* 認証のプロバイダー */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

   /* パスワードリセットの設定 */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'email' => 'auth.emails.password',
            'table' => 'password_resets',
            'expire' => 60,
        ],
    ],

];

となりました。違いは、guardsprovidersの導入です。

ちょっとこれではわかりにくいので、先の例を使って、ECサイトを想像してもらって、ショッピングをするユーザ画面と、サイトを管理する管理者画面があり、どちらもログインで認証が必要と仮定しましょう。ユーザー画面では買い物かごをチェックアウトするには、会員のログインが必要とします。

となると必要な設定は以下にようになります。

return [
    /* 認証のデフォルト設定 */

    'defaults' => [
        'guard' => 'users',
        'passwords' => 'users',
    ],                         

    /* 認証のガードを定義 */                                                   

    'guards' => [
        'users' => [
            'driver' => 'session',
            'provider' => 'users_provider',
        ],

        'admin_users' => [
            'driver' => 'session',
            'provider' => 'admin_users_provider',
        ],
    ],

    /* 認証のプロバイダー */

    'providers' => [
        'users_provider' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        'admin_users_provider' => [
            'driver' => 'eloquent',
            'model' => App\AdminUser::class,
        ],
    ],

   /* パスワードリセットの設定 */

    'passwords' => [
        'users' => [
            'provider' => 'users_provider',
            'email' => 'auth.emails.password',
            'table' => 'password_resets',
            'expire' => 60,
        ],
    ],

];

ガードには、会員ログインのためのusersと、管理者ログインのためのadmin_usersの2つを定義します。どちらもセッションを使って、ログイン後の画面をプロテクトします。また、それらのプロバイダーで定義されているように、会員のUsersと管理者のAdminUsersのエロクエントモデルが認証のための情報提供元となります。

guardsprovidersの概念を導入することにより、今までの1つだけの認証のメカニズムを複数としたわけです。

さて、次はこの設定の使用です。このファイル以外で使用するのは、ガード名だけですから簡単です。

ガードを指定する場所はプログラムの中でいくつかありますが、以下のようにapp/Http/routes.phpで使用されるのが一番明確と思います。

例えば、ユーザー画面では、

    Route::group(['middleware' => 'guest:users'], function() {
        Route::get('login', 'user\AuthController@getLogin');
        Route::post('login', 'user\AuthController@postLogin');
        Route::get('signup', 'user\SignupController@getSignup');
        Route::post('signup', 'user\SignupController@postSignup');
        Route::get('password/email', 'user\PasswordController@getEmail');
        Route::post('password/email', 'user\PasswordController@postEmail');
        Route::get('password/reset/{token}', 'user\PasswordController@getReset');
        Route::post('password/reset', 'user\PasswordController@postReset');
    });

    Route:: group(['prefix' => 'member', 'middleware' => 'auth:users'], function() {
        Route::get('index', 'user\MemberController@getIndex');
        Route::get('password', 'user\MemberController@getPassword');
        Route::post('password', 'user\MemberController@postPassword');
        Route::get('profile', 'user\MemberController@getProfile');
        Route::post('profile', 'user\MemberController@postProfile');
        Route::get('logout', 'user\AuthController@getLogout');
    });

以前、ユーザー認証(4)認証でページを保護で説明したように、

ミドルウェアとして、guestauthが使われます。しかし、前回と違って、guest:usersのようにガード名を指定することが必要です。指定がないなら、auth.phpのデフォルトのセクションで指定したガードが自動的に使われます。

ちなみに、管理者側では、

Route::group(['prefix' => 'admin', 'middleware' => 'guest:admin_users'], function()
{
    Route::get('login', 'admin\AuthController@getLogin');
    Route::post('login', 'admin\AuthController@postLogin');
});

Route:: group(['prefix' => 'admin', 'middleware' => 'auth:admin_users'], function() {

    Route::get('logout', 'admin\AuthController@getLogout');
    Route::get('index', 'admin\HomeController@getIndex')->name('admin.home');
..

こんな感じです。

AuthControllerは、ユーザ画面と管理画面では、前回紹介した自動作成使われるもののコピーを編集する必用あります。

ユーザ画面では、

...

class AuthController extends BaseController
{
    protected $guard = 'users';

    protected $redirectTo = 'user/member/index';   // ログイン後のリダイレクト先
    protected $redirectAfterLogout = 'user/login';   // ログアウト後のリダイレクト先

    protected $username = 'email';               // ログインとなるDBの項目名

    protected $maxLoginAttempts = 5;             // ログインスロットルとなるまで最高のログイン失敗回数
    protected $lockoutTime = 60;                 // ログインスロットルとなってからの待ち秒数

    use AuthenticatesAndRegistersUsers, ThrottlesLogins;

    public function showLoginForm()
    {
        return view('user.login');  //テンプレートの場所を変える
    }
..

管理画面では、

...

class AuthController extends BaseController
{
    protected $guard = 'admin_users';

    protected $redirectTo = 'admin/index';   // ログイン後のリダイレクト先
    protected $redirectAfterLogout = 'admin/login';   // ログアウト後のリダイレクト先

    protected $username = 'login';               // ログインとなるDBの項目名

    protected $maxLoginAttempts = 5;             // ログインスロットルとなるまで最高のログイン失敗回数
    protected $lockoutTime = 60;                 // ログインスロットルとなってからの待ち秒数

    use AuthenticatesUsers, ThrottlesLogins;

    public function showLoginForm()
    {
        $form = new \stdClass();

        $form->login = Form::text('login', '',
            ['size' => 20, 'maxlength' => 20, 'class' => 'en', 'autofocus' => 'autofocus']);

        $form->password = Form::password('password',
            ['size' => 40, 'maxlength' => 20, 'class' => 'en']);

        return view('admin.login')->with(compact('form')); //テンプレートの場所を変える
    }
..

となります。認証が2つとなると、テンプレートなどいろいろな指定が必要となることに注意してください。上の例では、会員のログインは、emailですが、管理者のログインは、emailでなくてもよい文字列という仮定です。

最後に、ログイン後にログインしたユーザーの情報がほしいときは、今まで、

Auth::user()

でしたが、マルチ認証となると、

Auth::guard('users')->user()
Auth::guard('admin_users')->user()

と明確にガード名を指定する必用があります。

ユーザー認証(9)Laravel 5.2 コンポーネント自動作成

Laravelの5.2が登場してきました。いくつか興味ある変更がありますが、ユーザ認証に関してはスタート地点が身近になりました。

以下のコマンドを実行するだけで、

php artisan make:auth

以下を自動的に作成してくれます。

  1.  画面とEメールのテンプレート
    • ログイン画面
    • ユーザー登録画面
    • パスワードリセット情報送信画面
    • パスワードリセットのEメール
    • パスワードリセット画面
    • ホーム画面(ログイン後)
  2.  ホームコントローラ(HomeController.php)
  3.  ルータの設定の変更 (routes.php)

データベースを用意して、.envの設定ファイルを編集すれば、以下のように。

Laravel 2015-12-27 13-58-55

Laravel 2015-12-27 13-56-14
画面は、Bootstrapを使用していて、ちょっと編集すればすぐに実践で使えそうです。

ユーザー認証(8)ユーザーの有効・無効を考慮

まだまだ続くユーザ認証。今まではいわば基礎編みたいなもので、教科書通りの紹介。今回は、実践として、こういうときはどうする?みたいな応用編です。

まずはDBテーブルusersの構成を見るところから。

+----------------+------------------+------+-----+---------------------+----------------+
| Field          | Type             | Null | Key | Default             | Extra          |
+----------------+------------------+------+-----+---------------------+----------------+
| id             | int(10) unsigned | NO   | PRI | NULL                | auto_increment |
| active_flag    | char(1)          | NO   |     |                     |                |
| 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        | NO   |     | 0000-00-00 00:00:00 |                |
| updated_at     | timestamp        | NO   |     | 0000-00-00 00:00:00 |                |
+----------------+------------------+------+-----+---------------------+----------------+

ごく簡単な構成です。しかし、実際にはユーザーはすでに退会したかとか、問題があるユーザーをブロックする必要がある、のような理由で、有効・無効のフラッグをつけるのが一般的です。

ということで、active_flagとしてusersのフィールドを追加してみましょう。このactive_flagは、char(1)として有効はY、無効はNの値とします。

migrationを作成して、以下のように編集して実行。

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddActiveFlagToUsers extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function ($table) {
            $table->char('active_flag', 1)->after('id');
        });
    }
...
}

さて、今度は認証のプログラムの変更です。ユーザーのログインとパスワードがマッチかつユーザーが有効のときだけに認証を成功としたいです。さて、どこを変更すればよいのでしょうか?

これは以下のAuthController.phpにトレイトで定義されているメソッドの追加となります。

namespace App\Http\Controllers\Auth;

use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers, ThrottlesLogins {
        getCredentials as getCredentialsTrait;
    }

    public function __construct()
    {
        $this->middleware('guest', ['except' => 'getLogout']);

        $this->maxLoginAttempts = 5;
        $this->lockoutTime = 60;
    }

    protected function getCredentials(Request $request)
    {
        $credentials = $this->getCredentialsTrait($request);

        $credentials['active_flag'] = 'Y';

        return $credentials;
    }
    ...

getCredentialsは、AuthenticateAndRegistsUsersトレイトの中で使用されているAuthenticatesUsersのトレイト。以下を参照してください。

ユーザー認証(3)ログイン・ログアウト

ここで同じ名前のメソッドを使用して、クレデンシャルにユーザーが有効、つまり active_flag => 'Y'という条件を追加したいです。
しかし、ここではまず、すでに定義されているgetCredentialsの関数をコールしてからの追加としたい。問題は、

これでは、再帰となるし、

$credentials = $this->getCredentials($request);

スタテックのメソッドでも、継承したクラスのメソッドでもないのでこうともできない。

$credentials = parent::getCredentials($request);

それゆえに、

    use AuthenticatesAndRegistersUsers, ThrottlesLogins {
        getCredentials as getCredentialsTrait;
    }

として、オリジナルのメソッド名を改名して、

$credentials = $this->getCredentialsTrait($request);

となったわけです。

ユーザー認証(7)スロットルのユニットテスト

前回話したログインのスロットル機能。もちろん手動、つまりブラウザ、で確認はできます。しかし、手動ではいちいち面倒ですね。自動で動作を確認できたらもっと良いです。そこで登場するのがユニットテスト。難しくはないです。チェックしたいのは、

  1. パスワードが正しいときのログインが成功するかのテスト
  2. パスワードを間違えて、失敗回数がデフォルトの5回となったときに、スロットされるかのテスト
  3. その後デフォルトの60秒待って、パスワードが正しいときに成功するかのテスト

他にもいくつか考えられますが、最低限は以上としてテストを作成してみましょう。

注意:パスワードには理解しやすいように日本語としていますが、実際は英数字が一般的です。

class LoginTest extends TestCase
{
    public function testLoginSuccess()
    {
        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('正しいパスワード', 'password')
            ->press('保存')
            ->see('ホームページ');
    }

    public function testLoginThrottle()
    {
        for($i=1;$i <=5; $i++)
        {
            $this->visit('/auth/login')
                ->type('test@gmail.com', 'email')
                ->type('間違ったパスワード', 'password')
                ->press('ログイン')
                ->see('Eメールとパスワードにマッチするレコードがありません。');
        }

        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('間違ったパスワード', 'password')
            ->press('ログイン')
            ->see('ログインの失敗回数が設定を超えました。次回のログインまで60秒お待ちください。');

        sleep(10);

        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('正しいパスワード', 'password')
            ->press('ログイン')
            ->see('ログインの失敗回数が設定を超えました。');

        sleep(50);

        $this->visit('/auth/login')
            ->type('test@gmail.com', 'email')
            ->type('正しいパスワード', 'password')
            ->press('ログイン')
            ->see('ホームページ');
    }
}

最初のテスト、testLoginSuccessは、正しいパスワードを入力したときのテストで、その後「ホームページ」という文が入ったページへ行くとする仮定。

次のtestLoginThrottleは、スロットルされるまでの失敗回数の確認とスロットル解除の確認のテスト。まず間違ったパスワードを入れて5回失敗させます。画面には、「Eメールとパスワードにマッチするレコードがありません」のエラーメッセージが表示されます。その後、さらに失敗すると、今度はログインをスロットルしたメッセージが表示されます。ここから60秒経過しないと次のログインができません。確認のために10秒後に正しいパスワードでログインしてみましょう。そこでは、さらに「ログインの失敗回数が設定を超えました。」を含むエラーメッセージがでます。残りの秒数は誤差が出ると思うので、その部分の確認はしません。そして、50秒後(つまり、スロットル開始から60秒後)には、正しいパスワードでログインが成功となるはずです。

さて、ここで注意です。Laravelに含まれるphpunitのテストの設定ファイルは、以下の内容です。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">app/</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

前回話したように、ログイン失敗回数や現在スロットルされていることなどの情報は、デフォルトではファイルを使用して行っています。しかし、テストでは CACHE_DRIVERは配列設定となっています。私のテストでは、そのためか上のテストはスロットル期間終了後に成功とはなりませんでした。どうも残りの秒数のカウントが負となっていしまい、うまくいかない。

この設定をphpunit.xmlでarrayからfileあるいはdatabaseに変更とすると、うまく行きました。

ユーザー認証(6)ログインスロットル

スロットルと聞くと、どうしてもバイクのアクセルを想像してしまいます。どちらかというとスピードを出すために。スロットルは正確にはスピードを抑圧する意味で、「スロットル全開」というと、抑制なしで最高のスピードを出すということになります。

ここでのスロットルは、パスワードを意図的に変えて不正にログインしようという試み、たいていはプログラムによる機械的な攻撃、を抑える目的のセキュリティ対策です。具体的には、例えば過去5分間に5回ログインを失敗すると、次回のログインには10分間待たなければならないという仕組みです。もちろんそれですべて防御できるとは言えないですが、少なくとも抑制にはなります。

嬉しいことにこれも、Laravelに機能があります。

スロットル機能はログインで行われるので、以前に説明したログインのプログラムを再度見てみましょう。

まず、AuthControler

namespace App\Http\Controllers\Auth;

use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers, ThrottlesLogins;

    ...

ThrottlesLoginsのトレイトがありますね。そして、AuthenticatesAndRegistersUsersのトレイトの定義で使用されている、AuthenticatesUsersのトレイト、以前の紹介ではスロットルの部分を省略したので、今度はそこの部分を含めて見てみましょう。以下のコードのコメントを見てください。

namespace Illuminate\Foundation\Auth;
use Illuminate\Http\Request;ろぐ
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Lang;

trait AuthenticatesUsers
{
    use RedirectsUsers;

    ...

    public function postLogin(Request $request)
    {
        $this->validate($request, [
            $this->loginUsername() => 'required', 'password' => 'required',
        ]);

        // ThrottlesLoginsは、上のAuthControllerのトレイトして使われているから、$throttleには値を含みます。
        $throttles = $this->isUsingThrottlesLoginsTrait();

    //ここで指定のログインの失敗回数を超えているなら、
       //画面にエラーメッセージを表示して次のログインを指定の時間待たせます。
        if ($throttles && $this->hasTooManyLoginAttempts($request)) {
            return $this->sendLockoutResponse($request);
        }

        //入力の値でログインを試みます。
        $credentials = $this->getCredentials($request);
        if (Auth::attempt($credentials, $request->has('remember'))) {
            return $this->handleUserWasAuthenticated($request, $throttles);
        }

    //ログインが失敗なら、ログインの失敗回数を1増やします。
        if ($throttles) {
            $this->incrementLoginAttempts($request);
        }
        return redirect($this->loginPath())
            ->withInput($request->only($this->loginUsername(), 'remember'))
            ->withErrors([
                $this->loginUsername() => $this->getFailedLoginMessage(),
            ]);
    }
 
    ...
}

というようにログインの失敗回数を追跡しています。

さて、何回まで失敗とか失敗回数を超過したら何分ログインを許さないかはどこで指定されているのでしょう。今度はトレイトのThrottleLoginsを見てみましょう。

namespace Illuminate\Foundation\Auth;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiter;
use Illuminate\Support\Facades\Lang;

trait ThrottlesLogins
{
    //ここです、失敗回数をチェックしているのは。
    protected function hasTooManyLoginAttempts(Request $request)
    {
        return app(RateLimiter::class)->tooManyAttempts(
            $request->input($this->loginUsername()).$request->ip(),
            $this->maxLoginAttempts(), $this->lockoutTime() / 60
        );
    }

    ...

    protected function maxLoginAttempts()
    {
        return property_exists($this, 'maxLoginAttempts') ? $this->maxLoginAttempts : 5;
    }

    protected function lockoutTime()
    {
        return property_exists($this, 'lockoutTime') ? $this->lockoutTime : 60;
    }
}

最初のhasTooManyLoginAttemptsのメソッドでは、maxLoginAttemptslockoutTimeのメソッドがコールされていますね。

それぞれ、ここのクラスのインスタンスで、maxLoginAttemptslockoutTimeの変数が定義されているなら、その値を使いますが、定義なしでは、最高5回、ログインロックは60秒という設定です。

また、失敗回数は、ログイン+IPアドレスの値がキーとなっています。つまり、同じログインで同じIPからログインを試みて連続5回失敗したら、1分のログインロックとなるということです。

最後に、これらの失敗回数の追跡やロックアウトの保持のデータは、どこで管理しているのでしょう。

先のコードで以下の参照をみてください。

use Illuminate\Cache\RateLimiter;

Cacheとして管理しているようです。Laravelでは、Cacheはデフォルトでファイルで管理しています。しかし、ファイルではなくデータベースや高速なメムキャッシュでのデータの管理も可能です。

config/cache.phpにおいて設定するか、.envにおいて、

CACHE_DRIVER=database

と設定することも可能です。データベースと設定するなら、以下のようにmigrationを通して、

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCacheTable extends Migration
{
    public function up()
    {
                Schema::create('cache', function($table) {
                        $table->string('key')->unique();
                        $table->text('value');
                        $table->integer('expiration');
                });
    }

    public function down()
    {
        //
    }
}

としてcacheテーブルを作成してください。ここで以下のようにデータベースの中身を見ながらテストをすると、実際どのようなレコードが作成されるかわかります。

例えば、ログインに失敗すると、以下のようなレコードが作成されます。

mysql> select * from cache\G;
*************************** 1. row ***************************
       key: laraveltest@gmail.com66.87.77.84
     value: eyJpdiI6IlJQN1Awck9BVzlTWXdnMk4wb0ZOMnc9PSIsInZhb...
expiration: 1449946110
*************************** 2. row ***************************
       key: laraveltest@gmail.com66.87.77.84:lockout
     value: eyJpdiI6Im5pMmg0dm1iREl0TFIwNkkzZTV1V2c9PSIsInZhb...
expiration: 1449946259
2 rows in set (0.00 sec)

最初のレコードは、失敗回数を数えているレコードです。キー(key)として、Eメールアドレス+IPアドレスとなっています。キーの値(value)は暗号化されています。回数が設定を超えると、2番目のレコードが作成されます。キーの最後に:lockoutが追加されていることに注意してください。こちらのレコードの期限は、設定のブロック時間(デフォルトは60秒)を経過した日時となっています。期限が過ぎてアクセスしたときに、これらのレコードは削除されて再度、失敗回数を追跡してきます。

ユーザー認証(5)パスワードリセット

ユーザーログインがあるなら、パスワードを忘れることがあるのは当然。忘れたらなら、通常はログイン(たいていはEメール)を入力して、パスワードのリセットのリンクを受け取り、リンク先の画面で新規のパスワードを設定します。新しいパスワードを作成して送信してくるサイトもあります。しかし、良く利用するサイトなら、やはり自分が覚えられるパスワードを設定したいです。

そう、この機能もLaravelで提供しています。

まずは、この機能に必要なものをリスト。

  • パスワードリセットリンクを送信してもらう画面
  • パスワードリセットリンクを含むメールのテンプレート
  • 送信するパスワードリセットリンクの有効期限を設定し保持するDBテーブル
  • パスワードリセットリンク先の画面で、新規パスワードを設定する画面

以上です。

パスワードリセットリンクを送信してもらう画面

routes.phpの設定から見てみましょう。

Route::get('password/email','Auth\PasswordController@getEmail');
Route::post('password/email', 'Auth\PasswordController@postEmail'); 

PasswordController.phpは、AuthController.phpと同様にlaravelインストール時に app/Http/Controllers/Authに存在します。

中身は以下にあるように、これまたAuthController.phpと同様にトレイトで構成されているのでほぼ空状態。

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;

class PasswordController extends Controller
{
    use ResetsPasswords;

    public function __construct()
    {
        $this->middleware('guest');
    }
}

ミドルウェアguestが使用されているので、すでにログインしているなら使用できません。

テンプレートは、reources/views/auth/password.blade.phpのファイルとします。

<form method="POST" action="{!! url() !!}/password/email">
    {!! csrf_field() !!}

    <div>
        Eメール
        <input type="email" name="email" value="{{ old('email') }}">
    </div>

    <div>
        <button type="submit">パスワードリセットリンクを送信</button>
    </div>
</form>

これで画面の表示までは完了。次は、送信ボタンを押したときの処理。以下の3つ作業が必要です。

  • 入力バリデーション。Eメールのフォーマットのチェックだけでなく、ユーザーとしてそのEメールがDBに存在するかのチェック
  • ユニークなトークンを発行し、EメールとともにDBに保存
  • パスワードリセットの画面のURLとトークンを合わせてリンクとして、ユーザーのEメールに送信

この3つの作業が行われているか、PasswordControllerで使用されるトレイトのResetsPasswordsを見てみましょう。


namespace Illuminate\Foundation\Auth;
use Illuminate\Http\Request;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

trait ResetsPasswords
{
    public function getEmail()
    {
        return view('auth.password');
    }

    public function postEmail(Request $request)
    {
        $this->validate($request, ['email' => 'required|email']);
        $response = Password::sendResetLink($request->only('email'), function (Message $message) {
            $message->subject($this->getEmailSubject());
        });
        switch ($response) {
            case Password::RESET_LINK_SENT:
                return redirect()->back()->with('status', trans($response));
            case Password::INVALID_USER:
                return redirect()->back()->withErrors(['email' => trans($response)]);
        }
    }
  ...

postEmailがあり画面の入力を受け取り処理しています。入力したEメールのバリデーションがあります。しかしここでは、その他の作業は皆、Password::sendRsetLinkに任せています。もうちょっと追及してみましょう。

Password::sendResetLinkは、PasswordのクラスはFacadeで、実際は以下のPasswordBrokerのクラスが使用されています。sendResetLinkの定義がありますね。


namespace Illuminate\Auth\Passwords;
use Closure;
use UnexpectedValueException;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Contracts\Auth\PasswordBroker as PasswordBrokerContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class PasswordBroker implements PasswordBrokerContract
{
    protected $tokens;
    protected $users;
    protected $mailer;
    protected $emailView;
    protected $passwordValidator;

    public function __construct(TokenRepositoryInterface $tokens,
                                UserProvider $users,
                                MailerContract $mailer,
                                $emailView)
    {
        $this->users = $users;
        $this->mailer = $mailer;
        $this->tokens = $tokens;
        $this->emailView = $emailView;
    }

    public function sendResetLink(array $credentials, Closure $callback = null)
    {
        $user = $this->getUser($credentials);
        if (is_null($user)) {
            return PasswordBrokerContract::INVALID_USER;
        }
        $token = $this->tokens->create($user);
        $this->emailResetLink($user, $token, $callback);
        return PasswordBrokerContract::RESET_LINK_SENT;
    }

    public function emailResetLink(CanResetPasswordContract $user, $token, Closure $callback = null)
    {
        $view = $this->emailView;
        return $this->mailer->send($view, compact('token', 'user'), function ($m) use ($user, $token, $callback) {
            $m->to($user->getEmailForPasswordReset());
            if (! is_null($callback)) {
                call_user_func($callback, $m, $user, $token);
            }
        });
    }
...

$this->getUserでDBテーブルusersに入力したEメールのレコードを取得して、$this->tokens->createでトークンを作成しDBに保存し、$this->emailResetLinkでパスワードリセットリンクを含むメールを送信します。マッチするDBレコードが存在しないならエラーコードを返して画面にエラーを表示となります。

ここにおいてメール送信に関していくつか。

まず、メール送信が行われるので、.envにおいてMAIL_DRIVERなどの設定が必要です。
さらに、config/mail.phpにおいて、fromの設定が必要です。

...
   'from' => ['address' => null, 'name' => null],
...

上のnullには、例えば、’support@gmail.com’、’サポート’のような具体的な値に置き換える必要あります。

次に、送信メールのテンプレートをresources/views/emails/password.blade.phpとして作成する必要あります。

以下のリンクをクリックして、パスワードのリセットができます。

{{ url('password/reset/'.$token) }

パスワードリセットリンク先の画面で、新規パスワードを設定する画面

さて、次はこのリンク先のパスワードリセット画面です。

まずは、routes.phpの設定から、

Route::get('password/reset/{token}', 'Auth\PasswordController@getReset');
Route::post('password/reset', 'Auth\PasswordController@postReset');

こちらもまた、PasswordControllerですね。

テンプレートは、reources/views/auth/reset.blade.phpのファイルとします。hiddenのtokenを忘れなく。

<form method="POST" action="{!! url() !!}/password/reset">
    {!! csrf_field() !!}
   <input type="hidden" name="token" value="{{ $token }}">
    <div>
        Eメール
        <input type="email" name="email" value="{{ old('email') }}">
    </div>

    <div>
        パスワード
        <input type="password" name="password">
    </div>

    <div>
        パスワードの確認
        <input type="password" name="password_confirmation">
    </div>

    <div>
        <button type="submit">パスワードをリセット</button>
    </div>
</form>

さて、この画面でボタンをクリックしたときの処理は、

  • 入力バリデーション。Eメールやパスワードの値チェック。EメールがDBに存在するかのチェック
  • DBのパスワードの値を暗号化して更新

でしょうか?見てみましょう。

まずは、先ほど出てきたトレイトのResetPasswords.php

...
    public function getReset($token = null)
    {
        if (is_null($token)) {
            throw new NotFoundHttpException;
        }
        return view('auth.reset')->with('token', $token);
    }
..
    public function postReset(Request $request)
    {
        $this->validate($request, [
            'token' => 'required',
            'email' => 'required|email',
            'password' => 'required|confirmed|min:6',
        ]);
        $credentials = $request->only(
            'email', 'password', 'password_confirmation', 'token'
        );
        $response = Password::reset($credentials, function ($user, $password) {
            $this->resetPassword($user, $password);
        });
        switch ($response) {
            case Password::PASSWORD_RESET:
                return redirect($this->redirectPath())->with('status', trans($response));
            default:
                return redirect()->back()
                            ->withInput($request->only('email'))
                            ->withErrors(['email' => trans($response)]);
        }
    }
...

入力バリデーションには、トークンが必須ですね。そして、Password::resetで処理を行い、その結果$responseが成功なら、指定のページにリダイレクト。エラーなら、同画面でエラー表示。

Password::resetの中身ですね、問題は。

こちらも、先のPasswordResetBroker.phpでコードされています。


...
    public function reset(array $credentials, Closure $callback)
    {
         $user = $this->validateReset($credentials);
        if (! $user instanceof CanResetPasswordContract) {
            return $user;
        }
        $pass = $credentials['password'];

        call_user_func($callback, $user, $pass);
        $this->tokens->delete($credentials['token']);
        return PasswordBrokerContract::PASSWORD_RESET;
    }

    protected function validateReset(array $credentials)
    {
        if (is_null($user = $this->getUser($credentials))) {
            return PasswordBrokerContract::INVALID_USER;
        }
        if (! $this->validateNewPassword($credentials)) {
            return PasswordBrokerContract::INVALID_PASSWORD;
        }
        if (! $this->tokens->exists($user, $credentials['token'])) {
            return PasswordBrokerContract::INVALID_TOKEN;
        }
        return $user;
    }

    public function validateNewPassword(array $credentials)
    {
        list($password, $confirm) = [
            $credentials['password'],
            $credentials['password_confirmation'],
        ];
        if (isset($this->passwordValidator)) {
            return call_user_func(
                $this->passwordValidator, $credentials) && $password === $confirm;
        }
        return $this->validatePasswordWithDefaults($credentials);
    }
...

resetでは、$this->validateResetで、ユーザーレコードの有無。新規パスワードのバリデーション、そしてトークンの存在と有効期限内かのチェックを行い、callbackでDBレコードのパスワードの更新を行います。そして最後に使用したトークンの削除となります。なるほど、削除しないと再度のリセットが可能になるので必要なわけですね。

ユーザー認証(4)認証でページを保護

ユーザー認証の目的は、ユーザー本人であることを確認し、ユーザーのプライベートの情報を他に見られることを防ぐことです。

Laravelでは、アプリにおけるすべてのルートを設定するroutes.phpのファイルにおいて、ミドルウェアを利用して保護するページを指定します。

Route::get('/home', ['middleware' => 'auth', function () {
    return view('auth.home');
}]);

上の例では、/homeを保護するために、ミドルウェアのauthを使用しています。authは、以下のKernel.phpで登録されているサービスです。


namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
    protected $middleware = [
   ...
    ];

    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    ];
}

そこでバインドされているクラスは、以下のAuthenticate.phpで定義されています。


namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate
{
    protected $auth;

    public function __construct(Guard $auth)
    {
        $this->auth = $auth;
    }

    public function handle($request, Closure $next)
    {
        if ($this->auth->guest()) {
            if ($request->ajax()) {
                return response('Unauthorized.', 401);
            } else {
                return redirect()->guest('auth/login');
            }
        }
        return $next($request);
    }
}

handle()内での、$this->auth->guest()は、ユーザーがゲスト、つまり認証されていないユーザーであるかどうかをチェックします。認証されていないなら、auth/loginにリダイレクトして、ログイン画面が表示されます。

routes.phpでのページ保護は、Route::groupで行うこともできます。例えば、以下のように複数のルートをまとめることができます。わかりやすいですね。

Route:: group(['prefix' => 'member', 'middleware' => 'auth'], function() {
    Route::get('index', 'MemberController@getIndex');
    Route::get('password', 'MemberController@getPassword');
    Route::post('password', MemberController@postPassword');
    Route::get('profile', 'MemberController@getProfile');
    Route::post('profile', 'MemberController@postProfile');
    Route::get('logout', 'MemberController@getLogout');
});

さらに、認証の保護しないページにおいて、すでに認証されているならスキップしてリダイレクトさせることも可能です。
以下では、ログインや登録画面にアクセスしたときにすでにログインしているなら、/homeにリダイレクトされます。ミドルウェアにおいて、guestが使用されていることに注意してください。このミドルウェアも先のKernel.phpで登録されています。

Route::group(['middleware' => 'guest'], function() {
    Route::get('login', 'LoginController@getLogin');
    Route::post('login', 'LoginController@postLogin');
    Route::get('signup', 'SignupController@getSignup');
    Route::post('signup', 'SignupController@postSignup');
});

これらのミドルウェアは、routes.phpだけでなくコントローラの中でも実行できます。例えば、前回まで使用してきたAuthController.php


namespace App\Http\Controllers\Auth;

use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    use AuthenticatesAndRegistersUsers, ThrottlesLogins;

    public function __construct()
    {
        $this->middleware('guest', ['except' => 'getLogout']);
    }

  ...
}

guestのミドルウェアをコンストラクタで使用することにより、ログアウト(getLogout)以外のメソッド、つまり会員登録(getRegisterpostRegister)とログイン(getLoginpostLogin)のメソッドにおいて、すでにログインしているなら/homeにリダイレクトします。

Top