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

Laravelの日本語レポジトリ

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

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

このレポジトリには、

  • デフォルトのユーザー認証の機能:会員登録、パスワードリセット、会員ログイン
  • 日本語に翻訳されたデフォルトのテンプレートと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

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

$ chmod -R a+w storage

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

$ composer install

.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の値をセッションの変数名として使い分けているわけです。

ユーザー認証(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()

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

Top