You are here
Home > Posts tagged "ユーザー認証"

ユーザー認証のテスト(3) Laravel 5.4 ログアウト

前回のログインのテストに対して、今回はログアウトのテスト。

まず、以下を見てください。

   /** @test */
    public function logout()
    {
        // ユーザーを1つ作成
        $user = factory(User::class)->create();

        // 認証済み、つまりログイン済みしたことにする
        $this->actingAs($user);

        // 認証されていることを確認
        $this->assertTrue(Auth::check());

        // ログアウトを実行
        $response = $this->post('logout');

        // 認証されていない
        $this->assertFalse(Auth::check());

        // Welcomeページにリダイレクトすることを確認
        $response->assertRedirect('/');
    }

前回と違うのは、ログインの実行$response = $this->post(..)が今回のテストにはないことです。その代わりに、actingAs()を利用して、プログラムで認証済みにしてしまいます。

actingAs()のおかげで、コードが短くなりわかりやすくなっただけでなく、その使用では以下のようにセッション値を入れたり、リダイレクトすることも可能です。認証された後の、つまりパスワード保護された中の機能のテストではいつも必要なことなので、将来重宝しそうです。

..
 $response = $this->actingAs($user)
                   ->withSession(['foo' => 'bar'])
                   ->get('/');
...

さて、テストとは関係ないですが、ログアウト後はどうして、/loginでなく/にリダイレクトされるのでしょう?また、その変更は可能なのでしょうか?

リダイレクト先は、残念ながら、以下のlaravelのパッケージの中のファイルでハードコードされています。

vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticateUsers.php

...
   /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        return redirect('/');
    }
...

上のファイルは編集するべきではありません。しかし、ログアウト後のリダイレクト先を変更したいなら、

app/Http/Controllers/Auth/LoginController.php

に、上のlogoutのコードを追加して、リダイレクト先を編集すればよいです。親のメソッドが上書きされて変更されることになります。use Illuminate\Http\Request;の行の追加も忘れないように。以下に全コードを掲載します。

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

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

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        return redirect('/');
    }
}

最後に今回のテストのコードは以下から利用可能です。

https://github.com/lotsofbytes/larajapan/blob/l54-test/tests/Feature/LoginTest.php

ユーザー認証のテスト(2) Laravel 5.4 ログイン

前回は、

  • ログイン画面にアクセスできる
  • ログインしないで、ホームページにアクセスするとログイン画面にリダイレクトされる

のテストを作成しました。

今回は、同じくログイン画面において以下のテストを作成します。

  • 登録されているユーザーのEメールとパスワードでログインできる
  • 間違ったパスワードでログインした場合、ログイン失敗してエラーが出力される

モデルファクトリ

さて、前回と違って今回は、ログインのテストゆえにテストのデータベースにユーザーのレコードが必要となります。もちろん、手動でレコードを作成しておいてテストも可能です。しかし、テストが複雑になって様々なケースのテストが増えてくると非常に面倒です。

そこで登場するのは、モデルファクトリ。テストのために、必要なデータを必要な数だけ自動生成してDBレコードを作成してくれます。ファクトリの設定は、LaravelのEloquentのモデルを指定するのでとても簡単。以下ではUserのモデルをもとにしています。Fakerのパッケージを利用しているので各項目での詳細はそちらを参照してください。

use Faker\Generator as Faker;

/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\User::class, function (Faker $faker) {
    static $password;

    return [
        'email'          => $faker->unique()->safeEmail, // 複数レコード作成時には重複しないEメールを生成
        'password'       => $password ?: $password = bcrypt('test1234'), // 複数レコード作成時には同じパスワードを生成
        'name'           => $faker->name,
        'remember_token' => str_random(100), //ランダム値
    ];
});

上の設定では、複数のレコード作成時には、emailは重複しないように、パスワードはstatic変数を利用してどのレコードも同じパスワードを作成するようなっています。bcryptは、LaravelのヘルパーでHash::makeと同じです。

日本語のデータを生成したいなら、config/app.phpで、

..
    /*
    |--------------------------------------------------------------------------
    | Application Locale Configuration
    |--------------------------------------------------------------------------
    |
    | The application locale determines the default locale that will be used
    | by the translation service provider. You are free to set this value
    | to any of the locales which will be supported by the application.
    |
    */

    'locale' => 'ja',
    'faker_locale' => 'ja_JP',
..

と、faker_localeを設定してください。

テストでレコードの自動作成

ユニットテストでは、毎回のテストにおいてテストDBを空にしてモデルファクトリで新しいデータを埋めます。これを実行するために、Laravelで2つの異なるメカニズムが用意されています。

  • DataMigrations:Laravelのmigrationのメカニズムを使用して、毎回テストごとにDBテーブルを1から作成する
  • DatabaseTransactions:DBのトランザクションの機能を利用して、毎回テストごとにDBテーブルを空にする

違いは重要で、前者はテストDBにテーブルが何も存在しなくてもOKですが、後者では空のDBテーブルが必要です。

前者は、新規のプロジェクトで最初からLaravelのmigrationを使用してDBを管理しているならお薦めですが、既存のプログラムをLaravelに移行したプロジェクト(私の場合はこれが多い)では、違うメカニズムでDBを管理しているため前者が使用できないので後者となります。

後者では、本DBと同じDB構造とするため、以下のようにmysqldumpなどを使用してコピーする必要あります。

$ mysqludmp -u root larajapan -p -d > larajapan_test.sql
$ mysql -u root larajapan_test -p < larajapan_test.sql

ここでは、すでにmigrationの設定がなされているうえに、DatabaseMigrationsのトレイトをユニットテストの中で宣言します。

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
//use Illuminate\Foundation\Testing\DatabaseTransactions;

use App\User;

class LoginTest extends TestCase
{
    use DatabaseMigrations;
..

ログインテスト

準備整ったところで、テストしてみましょう。まずは、ログイン成功のテスト。

先ほど設定したファクトリを使用して、最初に1つだけユーザーのレコードを作成します。その後、作成したのと同じパスワードを使用してログインを実行。実行後は、認証されていることと、ホームページにリダイレクトされることを確認します。

    /** @test */
    public function valid_user_can_login()
    {
        // ユーザーを1つ作成
        $user = factory(User::class)->create([
            'password'  => bcrypt('test1111')
        ]);

        // まだ、認証されていない
        $this->assertFalse(Auth::check());

        // ログインを実行
        $response = $this->post('login', [
            'email'    => $user->email,
            'password' => 'test1111'
        ]);

        // 認証されている
        $this->assertTrue(Auth::check());

        // ログイン後にホームページにリダイレクトされるのを確認
        $response->assertRedirect('home');
    }

次はログイン失敗のテスト。ここでもファクトリで1つだけレコードを作成しますが、わざと作成したのとは違うパスワードを使用してログインして、エラーメッセージが表示されることを確認します。

    /** @test */
    public function invalid_user_cannot_login()
    {
    // ユーザーを1つ作成
        $user = factory(User::class)->create([
            'password'  => bcrypt('test1111')
        ]);

        // まだ、認証されていないことを確認
        $this->assertFalse(Auth::check());

        // 異なるパスワードでログインを実行
        $response = $this->post('login', [
            'email'    => $user->email,
            'password' => 'test2222'
        ]);

        // 認証失敗で、認証されていないことを確認
        $this->assertFalse(Auth::check());

        // セッションにエラーを含むことを確認
        $response->assertSessionHasErrors(['email']);

        // エラメッセージを確認
        $this->assertEquals('メールアドレスあるいはパスワードが一致しません',
            session('errors')->first('email'));

$responseに使用されるassertの関数は、phpunitからのassertTrueと違い、Laravelで宣言されているものです。

参照としては、本サイトの以下

https://laravel.com/docs/5.4/http-tests#available-assertions

ですが、そこには掲載されていない関数もあるので、以下もチェックしてください。

https://laravel.com/api/5.4/Illuminate/Foundation/Testing/TestResponse.html

最後に、テストを実行してみましょう。

$ vendor/bin/phpunit --filter LoginTest
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 1.09 seconds, Memory: 16.00MB

OK (4 tests, 12 assertions)

エラーもなく成功ですね!

今回のコードは以下から利用可能です。

https://github.com/lotsofbytes/larajapan/blob/l54-test/tests/Feature/LoginTest.php

ユーザー認証のテスト(1) Laravel 5.4

Laravelでのユーザー認証は私のブログの中では最も人気のあるトピックです。今回は、私のLaravelの日本語のレポジトリ(Laravel 5.4)のコードをもとに、ユーザー認証のテストに取り組んでいきます。

テストの種類

テストと言っても、いくつか種類があり、Laravel5.4からはtestsのディレクトリ構造も変わり、

tests
├── CreatesApplication.php
├── Feature
│   └── ExampleTest.php
├── TestCase.php
└── Unit
    └── ExampleTest.php

のようにFeatureUnitの2つのサブディレクトリができました。

Unitには、一般的にはユニットテストと呼ばれるもので、大方は画面の表示を伴わないModelのメソッドに対するテストを作成します。一方、Featureには、機能テストやアクセプタンステストとも言われ、複数のクラスの複数のメソッドが関わる主にコントローラの機能を検証するためで、あたかもユーザーがテストするようなテストを作成します。

私の今までの開発では、比較的作成しやすいUnitテストが主で、Featureテストは皆無に近く、人間のテスターがその仕事を行っています。人間をFeatureテストに置き換える予定はないですが、Laravelが提供するテストの環境がFeatureテストを作成しやすくなってきているので、この機会に習得しようということです。

Laravel5.4では、Featureテストのために、2つのテストのフレームワークが提供されています。

ここでは、まず、追加のパッケージのインストールも要らない、高速なHTTPテストを作成していきます。

準備

github.comにおいて以下のブランチを用意したので利用してください。

https://github.com/lotsofbytes/larajapan/tree/l54-test

インストールは以下を参照してください。

Laravelの日本語のレポジトリ(Laravel 5.4)

設定したら、必ず以下を実行してブランチを変えてください。

$ git checkout l54-test

また、そこでは、phpunit.xmlを編集も必要です。以下を参考にしてください。

<?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="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_URL" value="http://localhost"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="DB_HOST" value="localhost"/>
        <env name="DB_DATABASE" value="larajapan_test"/>
        <env name="DB_USERNAME" value="test"/>
        <env name="DB_PASSWORD" value="password"/>
        <env name="MAIL_DRIVER" value="log" />
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

テストのDBは.envで設定しているものと違うDBが必要なことに注意してください。

最初のテスト

もとからあるtests/Feature/ExampleTest.phpのファイルを、LoginTest.phpと改名して以下のように編集します。

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class LoginTest extends TestCase
{
    /** @test */
    public function user_can_view_login()
    {
        $response = $this->get('login');

        $response->assertStatus(200);
    }

    /** @test */
    public function unauthenticated_user_cannot_view_home()
    {
        $this->get('home')
        	->assertRedirect('login');
    }
}

最初のテストuser_can_view_loginは、ログイン画面が閲覧できるかどうかのテストです。
返ってくるHTTPのステータスのコードが200なら、成功ということです。これが404(ページが見つかりません)とかだと何かがおかしいということになります。

次のテストunauthenticated_user_cannot_view_homeは、認証が必要なホームページに、認証もなしにアクセスしてみます。もちろん、アクセスできないでログイン画面にリダイレクトされるはずです。

ここ、メソッドが連結されていることに気づきましたか?

最初のテストの例のように、以下のよう2つの文に分けても書くこともできます。

$response = $this->get('home');
$response->assertRedirect('login');

これらのテストの実行ですが、Laravelのインストールにより、すでにvendorのディレクトリにphpunitのパッケージもインストールされています。

ということで、

$ vendor/bin/phpunit
PHPUnit 5.7.23 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)
Time: 171 ms, Memory: 10.00MB

OK (3 tests, 4 assertions)

と実行してテストは皆成功となります。3 testsとあるのは、app/tests/Unit/ExampleTestがあるからです。
以下のようにフィルタを書ければ、LoginTestの中のテストだけや、user_can_view_loginの1つテストだけの実行も可能です。

$ vendor/bin/phpunit --filter=LoginTest
HPUnit 5.7.23 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 157 ms, Memory: 10.00MB

OK (2 tests, 3 assertions)

$ vendor/bin/phpunit --filter=user_can_view_login
PHPUnit 5.7.23 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 111 ms, Memory: 10.00MB

OK (1 test, 1 assertion)

Laravelの日本語のレポジトリ(Laravel 5.4)

Laravelの日本語のリポジトリをLaravel5.4に更新しました。もうLaravel5.5がリリースされていますが、Laravel5.5と違ってLaravel5.4は、php7でなくphp5.6のバージョンで動作する最後のバージョンなゆえに重要です。前回同様に、以下インストールの手順です。

Laravelにおいて新規のプロジェクト作成はとても簡単。コマンドラインでいくつかのコマンドを実行をちょちょいとすれば完了。しかし、インストールされるのは英語のプロジェクト。テンプレートやメッセージの翻訳をいちいちしなければ日本語のプロジェクトにはならない。

ここのプロセスを簡単にと、Laravelバージョン5.4をもとに、開発者のために日本語化したレポジトリを作成してみました。

このレポジトリには、

  • デフォルトのユーザー認証の機能:会員登録、パスワードリセット、会員ログイン
  • 日本語に翻訳されたデフォルトのテンプレートとEメールメッセージ
  • 日本語に翻訳されたデフォルトの入力エラーメッセージ
  • デバッグのためのDebugbarツール
  • ウェブ解析ツールGoogle Analyticsのトラッキングスクリプト

以上を含みます。

さらに、今回は、実際動作するデモとして以下に用意しました。
https://larajapan.lotsofbytes.com/larajapan

さて、このレポジトリのインストールは以下の手順で簡単にできます。

レポジトリのインストール

SSHを利用しているなら、以下をコマンドラインで実行してレポジトリをインストールします。

$ git clone git@github.com:lotsofbytes/larajapan.git

あるいは、Httpsを使用するなら、以下を実行します。

$ git clone https://github.com/lotsofbytes/larajapan.git

実行後は、larajapanのディレクトリが作成されますが、お好きな名前に改名してもらってOKです。

その後、

cd larajapan

それから、ファイルのパーミッションを与えるべく以下を実行。

$ chmod -R a+w bootstrap/cache
$ chmod -R a+w storage

インストール後は、以下を実行してください。

$ composer install

.envの編集

.env.example をコピーして、.env を作成し編集して以下のように設定します。*****の部分を適切な値に変更してください。

APP_NAME=私のララベル
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_DATABASE=*****
DB_USERNAME=*****
DB_PASSWORD=*****
MAIL_DRIVER=sendmail

その後、以下を実行して.env内のAPP_KEYを更新します。

$ php artisan key:generate

APP_DEBUG=trueこれによりDebugbarが画面下方に表示されます。Debugbarに関しては、Debugbarで楽々デバッグも読んでください。

また、
ANALYTICS=UA-XXXXXXのようにサイトのためにGoogleから取得したコードを設定すれば、Google Analyticsでウェブでのユーザーの動向が追跡できます。

DBを作成

.envで指定したDBを作成。

$ echo 'CREATE DATABASE larajapan CHARACTER SET utf8' | mysql -u root -p
php artisan migrate

ウェブサーバーの立ち上げ

最後に以下を実行して、ウェブサーバーを立ち上げると、

$ php artisan serve

以下のアドレスでブラウザーからアクセスできます。

http://localhost:8000

以上です。

上の設定は私の開発環境Fedora LinuxとAmazon Linux OSで、7.0のバージョンのPHPで、DBにはMysqlあるいはMaria DBを使用して動作確認しています。しかし、皆さんの環境ではいろいろ異なることがあると思います。問題や指摘があれば、ご連絡ください。

Laravelの日本語レポジトリ(Laravel 5.3)

** Laravelの日本語レポジトリをLaravel 5.4に更新したために手順において更新があります **

Laravelにおいて新規のプロジェクト作成はとても簡単。コマンドラインでいくつかのコマンドを実行をちょちょいとすれば完了。しかし、インストールされるのは英語のプロジェクト。テンプレートやメッセージの翻訳をいちいちしなければ日本語のプロジェクトにはならない。

ここのプロセスを簡単にと、Laravelバージョン5.3をもとに、開発者のために日本語化したレポジトリを作成してみました。

このレポジトリには、

  • デフォルトのユーザー認証の機能:会員登録、パスワードリセット、会員ログイン
  • 日本語に翻訳されたデフォルトのテンプレートとEメールメッセージ
  • 日本語に翻訳されたデフォルトの入力エラーメッセージ
  • デバッグのためのDebugbarツール
  • ウェブ解析ツールGoogle Analyticsのトラッキングスクリプト

以上を含みます。

さらに、今回は、実際動作するデモとして以下に用意しました。
https://larajapan.lotsofbytes.com/larajapan

さて、このレポジトリのインストールは以下の手順で簡単にできます。

レポジトリのインストール

SSHを利用しているなら、以下をコマンドラインで実行してレポジトリをインストールします。

$ git clone -b 5.3 git@github.com:lotsofbytes/larajapan.git

あるいは、Httpsを使用するなら、以下を実行します。

$ git clone -b 5.3 https://github.com/lotsofbytes/larajapan.git

実行後は、larajapanのディレクトリが作成されますが、お好きな名前に改名してもらってOKです。

その後、

cd larajapan

それから、ファイルのパーミッションを与えるべく以下を実行。

$ chmod -R a+w storage

インストール後は、以下を実行してください。

$ composer install

以下も必要かもしれません。

$ composer update

.envの編集

.env.example をコピーして、.env を作成し編集して以下のように設定します。*****の部分を適切な値に変更してください。

APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_DATABASE=*****
DB_USERNAME=*****
DB_PASSWORD=*****
MAIL_DRIVER=sendmail

その後、以下を実行して.env内のAPP_KEYを更新します。

$ php artisan key:generate

APP_DEBUG=trueこれによりDebugbarが画面下方に表示されます。Debugbarに関しては、Debugbarで楽々デバッグも読んでください。

また、
ANALYTICS=UA-XXXXXXのようにサイトのためにGoogleから取得したコードを設定すれば、Google Analyticsでウェブでのユーザーの動向が追跡できます。

DBを作成

.envで指定したDBを作成。

$ echo 'CREATE DATABASE larajapan CHARACTER SET utf8' | mysql -u root -p
php artisan migrate

ウェブサーバーの立ち上げ

最後に以下を実行して、ウェブサーバーを立ち上げると、

$ php artisan serve

以下のアドレスでブラウザーからアクセスできます。

http://localhost:8000

以上です。

上の設定は私の開発環境Fedora LinuxとAmazon Linux OSで、5.6のバージョンのPHPで、DBにはMysqlあるいはMaria DBを使用して動作確認しています。しかし、皆さんの環境ではいろいろ異なることがあると思います。問題や指摘があれば、ご連絡ください。

ログイン成功のイベント

ユーザー認証(11)Laravel 5.2 ログインの記録で、ログイン成功後の処理に関して説明しました。

しかし、前回のログインのRemember Meのポストのための調査で、この「Remember Me」がオンになっているときは、先のログイン成功後の処理は、最初のログインのときだけしか実行しないことを見つけました。

つまり、最悪のケース、5年間ログイン成功後の処理は実行されません。同じブラウザを使用してもIPアドレスが変わることがあるし、記録としても不十分となり不都合です。

さて、どう解決したらよいでしょうか?

これにはLaravelのイベントのメカニズムを利用します。イベントの仕組みは、イベントとリスナーの2つのクラスの定義から構成され、イベントのクラスのオブジェクトをプログラムの中でコール(ファイヤー)することでリスナーのオブジェクトのアクションが実行されます。

例えば、ログインが成功したときに希望するアクション(ログインの記録)を実行したいときは、ログイン成功のイベントとそのリスナーを作成します。

ありがたいことに、ユーザー認証に関してのイベントは、すでにIlluminiate\Auth\Eventsで定義されており、Illuminiate\Auth\SessionGuardのクラス内のメソッドで要所要所でファイヤーされています。

イベントの種類としては、以下が揃っています。

Attempting ログインを試みたとき
Authenticated ログイン成功後のセッションにアクセスするとき
Failed ログインが失敗したとき
Lockout ログイン連続失敗でロックアウトされたとき
Login ログインが成功したとき
Logout ログアウトしたとき
Registered 会員登録完了したとき

今回は、Loginのイベントが使えそうです。

コードをチェックしたところ、Remember Meがオンのときも、このイベントをファイヤーします。

最初にログイン成功したときだけでなく、デフォルトでは2時間アイドル後にセッションが切れるときや、ブラウザを閉じて再度オープンするときなど、再度ログインが必要なときにRemember Meの情報を使用して自動ログインするときにも、このイベントがファイヤーされます。

ということで、イベントのクラスはすでにあるので、このイベントに対応するリスナークラスを作成して登録すれば作業終了です。

ログイン成功のイベントLoginに対して、リスナーを作成してみましょう。逆になりますが、まず登録から始めます。

登録は、

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'Illuminate\Auth\Events\Login' => [
            'App\Listeners\LoginListener',
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

$listenの変数に、イベントに対応するクラスを指定するだけです。

次にリスナーのクラスを作成しますが、先の登録が完了していれば、便利なことに以下をコマンドラインで実行するだけで作成してくれます。

$ php artisan event:generate

もちろん、以下でも作成可能です。

$ php artisan make:listener LoginListener --event=Illuminate\Auth\Events\Login

作成された、リスナーを以下のように編集します。


namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Http\Request;

use App\UserLog;

class LoginListener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * Handle the event.
     *
     * @param  Illuminate\Auth\Events\Login  $event
     * @return void
     */
    public function handle(Login $event)
    {
        $user = $event->user;

        $ip = $this->request->ip();
        $agent = $this->request->header('User-Agent');

        UserLog::create([
            'user_id'     => $user->id,
            'ip'          => $ip,
            'hostname'    => gethostbyaddr($ip),
            'agent'       => $agent
        ]);
    }
}

これで、ユーザーがログインをしたときや、セッションが期限切れで失って自動ログインされるときに、ログインの記録のレコードを作成してくれます。

ログインのRemember Me

Laravel 5.3になって、認証の部分が変りました。それに関していっぱい紹介したいことありますが、それは将来でのポストとして、今回は、「ログインのRemember Me」に関して学んだことを紹介します。

以下の画面のようにログインには、

my-application

「Remember Me」あるいは「次から入力を省略」、ログインのときにオンとすると次回から毎回毎回ログインする必要ないよという便利な機能。巷ではどこでも見かけます。

Laravelでも、認証のパッケージのインストール、

$ php artisan make:auth

を実行すると、デフォルトでついてくる機能です。

さて、この「Remember Me」でどうやってログインを要らなくするのか、そのメカニズムを探ってみましょう。

まず、「Remember Me」をクリックしてログイン成功すると、以下のように2つのクッキーを作成します。

  • laravel_session

    通常のセッションのためのクッキー。期限は、

    ..
        'lifetime' => 120,
        'expire_on_close' => true,
    ..
        'cookie' => 'laravel_session',
    ..
    

    lifetimeあるいはexpires_on_closeの値で決定されます。

    デフォルトでは、前者が2時間(120分)、後者は、fale。後者をtrueとするとブラウザをクローズしたときに期限切れとなります。ちなみに、このクッキーの名前は、cookieの値で変更可能です。

  • remember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d

    こちらは、「Remember Me」のためのクッキーで、Laravelではログインした日から5年間と期限はハードコードされています。つまり、同じブラウザを使用し続けるなら、ログオフしない限り5年間ログインなしでアクセス可能です。

このクッキーが使用されるシナリオは簡単にはこうです。

ログイン後、2時間アイドルが続いたらあるいはブラウザを閉じたら、現在のセッションは無効になります。

セッションが無効となると、ユーザーの認証が不可能となります。なぜなら、セッションの中に含まれていたユーザーのIDの取得が不可能となるからです。そうなるとログインの画面に遷移して、再度ログインしなければなりません。

しかし、「Remember Me」をオンとしてログインしていたなら、この時点で、まだ期限までたっぷり時間がある「Remember Me」のクッキーを見に行きます。

このクッキーの中には、暗号化されたユーザーIDが入っているので、それを非暗号化して再度セッションを作成します。これにより、再度ログインをすることなしに、ログインした状態をキープすることが可能となるのです。

Laravel 5.3 コントローラのコンストラクタの重要な変更

Laravelのバージョン5.3がリリースされてから、かれこれ1ヶ月。使い始めてみました。

以前のバージョンからバージョン5.*への変更に比べれば、そう注意しなければならない変更はないのだけれど、とっても注意することありました。

Laravel5.2では、コントローラのコンストラクタでこんなこと可能でした。


namespace App\Http\Controllers;

use Illuminate\Http\Request;

use Auth;

class HomeController extends Controller
{
    protected $options;

    public function __construct()
    {
        $user = Auth::user();

        $this->options = $user->options; //ログインしたユーザーの設定オプションを取得
    }
}

認証されたユーザー、つまりログインしたユーザーの認証の情報にアクセスすることが、コントローラのコンストラクタで可能でした。

同じコントローラの他のメソッドで何回も同じことを行うのは面倒なわけで、また以下のようにすでにroutes.php(5.3では、app/routes/web.phpとなります)の設定で、パスワード保護されているので、認証されたユーザーの情報を共有できる理想の場所というわけです。

Route::get('login', 'Auth\AuthController@showLoginForm');
Route::post('login', 'Auth\AuthController@login');
Route::get('logout', 'Auth\AuthController@logout');

Route::group(['middleware' => 'auth'], function() {
	Route::get('home', 'HomeController@index');
});

しかし、それは「してはいけないこと」だったのです!Laravel5.3になるやいなや、認証したユーザーのAuth::userがnullを返すようになりました。

そして、Laravelの作者(Mr. Tayler)の思惑とは逆に、こんなことをやっている開発者はごまんといたわけです。

以下のやり取りをみていると、

突如の変更に困ったユーザーとそれに対応する作者たちの会話

作者は、ユーザー認証はクッキーやセッションがあってこそ成り立つものであり、それが入ってくるのはコントローラのメソッドにおいてのrequestの引数である。それゆえにrequestがないコンストラクタでその情報を取得するのはデザイン上「悪い」と。

しかし、コンストラクタはいろいろと共有できる便利な場所であり、しかも今までそこで問題なかったのだからね。

作者もそれを理解して、すぐに以下のような対応をしてくれました。


    public function __construct()
    {
        $this->middleware(function ($request, $next) {
            $this->options = Auth::user()->options;//ログインしたユーザーの設定オプションを取得

            return $next($request);
        });
    }

Laravel 5.3.4からの対応です。

ファイルのアップロード(8)プライベートに画像を表示

前回は、画像をパブリックに表示する方法を説明しましたが、今回は画像をプライベートに表示する方法です。

いくつか方法があります。

まず、前回のようにアップロードをパブリックの場所に保存して、特定のユーザーだけに表示のためのURLを教える。

しかし、DBから自動発行されるproduct_image_idを画像ファイル名に使用するなら、URLを操作することで他のファイルも見れてしまいます。

そうなら、画像のURLをわかりにくいように変えて、他の画像のURLを予想しにくくすることも可能です。

例えば、235.jpgとは見せずに、1f3870be274f6c49b3e31a0c6728957f.jpgにするとか。

それは、md5()を利用することで簡単に可能です。


    public function filename()
    {
        $ext = 'jpg';
 
        switch($this->mime)
        {
            case 'image/jpeg':
            case 'image/jpg':
                $ext = "jpg";
                break;
 
            case 'image/png':
                $ext = "png";
                break;
 
            case 'image/gif':
                $ext = "gif";
                break;
        }
 
        return sprintf("%d.%s", md5($this->product_image_id), $ext);
    }

よりセキュアにするには、DBに保存するときに、uniqid()あるいは、openssl-random-pseudo-bytes()を使用してランダムな値を生成して、その値をファイル名として保存するとか。要するに、IDのように連続な番号とはならないので、容易にファイル名を予測できないようにすることです。

しかし、究極は、ファイルをパブリックから見れない場所に保存して、それを表示する方法です。

例えば、storage/images/product/1.jpgのように、パブリックから見れないstorageのディレクトリに画像をアップロードするようにして、見せるときには、ログインしたユーザーと関連ある画像だけを、そのユーザーに表示する。

この場合は、パブリックに保存されている画像と違い、固定のURLを通してウェブサーバーに画像の表示を任せることはできません。逆に、あたかもウェブサーバーが画像ファイルを読んでデータをストリームするという作業と同じことをプログラムで行います。header()を使用すれば、そう難しいことではありません。

namespace App\Http\Controllers\User;

use Illuminate\Http\Request;

use Log;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Product;
use App\ProductImage;

class ProductController extends Controller
{
	public function getImage(Product $product)
	{
		return view('user/product_image', compact('product'));
	}

	public function downloadImage(ProductImage $product_image)
	{
		//@TODO ここで、認証したユーザーに画像を表示していいかどうかをチェック。
		//そうでないなら、空の画像を表示

		$filename = $product_image->filename();

		header("Content-type: $product_image->mime name=$filename");
		header("Content-Disposition: attachment; filename=$filename");
		header("Content-Length: ".@filesize($product_image->path));
		header("Expires: 0");
		@readfile($product_image->path);
		exit;
	}
}

上で使用されるテンプレートは、

@extends('user.layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">アップロードしたファイルを表示</div>
                <div class="panel-body">
                    <div>
                        @foreach ($product->product_images as $image)
                            <img src="{{ url('/user/product_image', $image->product_image_id) }}">
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

のようになります。

routes.phpは、以下のように認証保護された中でコールされます。


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

	Route::get('login', 'User\Auth\AuthController@showLoginForm');
	Route::post('login', 'User\Auth\AuthController@login');
	Route::get('logout', 'User\Auth\AuthController@logout');
..
	Route::group(['middleware' => 'auth:user' ], function () {
		Route::get('home', 'User\HomeController@index');
..
		Route::get('product/{product}/image', 'User\ProductController@getImage');
		Route::get('product_image/{product_image}', 'User\ProductController@downloadImage');
	});
});

ユーザー認証(14)Debugbar

マルチ認証のトピックが続いていますが、同じブラウザを使用して「会員」と「管理者」の両方にログインしたらどうなるのでしょう?

ログインはブラウザのクッキーを使用して、サーバーのセッションと繋がっています。Laravelはそれぞれに違うセッション、違うクッキーを使用するのでしょうか?それとも同じセッションで違う情報を保持するのでしょうか?興味ありませんか?

ということで、便利なツールの紹介とともに、認証のセッションがどうなっているかチェックしてみましょう。

ここで紹介するツールはDebugarというLaravelのためのツールです。

これをインストールすると、画面下にDebugarの情報パネルが表示され、実行したLaravelの様々な情報を見ることができます。

例えば、以下は会員のログイン成功直後の画面ですが、実行したDBのクエリーを見ることができます。DBテーブルusersに対するクエリーがあります。

debugbar-login

このツールのインストールはとても簡単です(GitHubにリポあります)。

まず、以下を実行します。

composer require barryvdh/laravel-debugbar

composer.jsonが更新されライブラリがダウンロードされインストールされます。

次に、config/app.phpに以下を追加します。

...
   'providers' => [
...
       Barryvdh\Debugbar\ServiceProvider::class,
   ],

   'aliases' => [
...
      'Debugbar' => Barryvdh\Debugbar\Facade::class,
   ],
];

これで完了です。

先の会員ログインを実行では、DBのクエリーの情報を見ることができましたが、セッションはどうなのでしょう?

debugbar-session
赤丸の部分がセッションの情報を見るボタンです。そして赤の四角の部分が、会員が認証されていることを示す情報です。login-user..の部分です。

さて、同じブラウザでもう1つタブを作成して今度は、管理画面にログインしてみましょう。以下が、ログイン成功後のセッション値です。

debuggar-session2

値が増えていますね。しかし、今度は、login-admin..と違う名前となっていますね。

つまり、セッションの中でguardの値をセッションの変数名として使い分けているわけです。

Top