目次

Apricot セッション認証

y2sunlight 2020-05-15

Apricot に戻る

関連記事

ミドルウェアを使ってユーザのセッション認証を実装します。本章ではApricotのユーザ認証機能を使用しているので、まだお読みでない方は先にそちらをご一読下さい。

セッション認証ではログイン画面を作成してユーザ認証を行います。この認証方法ではログアウト機能が有効で、Remember-Meトークンによる自動ログイン機能も実装します。比較的多くのユーザでサイトを運用する場合はこの認証方法をお薦めします。

本機能は次の2つの部分に分かれます。


ミドルウェア

SessionAuth クラス

以下に、セッション認証のミドルウェアを示します。

/apricot/app/Middleware/Auth

SessionAuth.php
<?php
namespace App\Middleware\Auth;
 
use Core\Foundation\Response;
use Core\Foundation\Invoker;
use Core\Foundation\Middleware\Middleware;
use App\Foundation\Security\AuthUser;
 
/**
 * Session認証 - Middleware
 */
class SessionAuth implements Middleware
{
    /**
     * Excludeing controller
     * @var array
     */
    private $exclude = [
        'AuthController',
    ];
 
    /**
     * Process incoming requests and produces a response
     * {@inheritDoc}
     * @see \Core\Foundation\Middleware\Middleware::invoke()
     */
    public function process(Invoker $next): Response
    {
        // When exclude controller
        if (in_array(controllerName(), $this->exclude))
        {
            return $next->invoke();
        }
 
        // Verify whether user is authenticated
        if (AuthUser::verify())
        {
            return $next->invoke();
        }
 
        // 未認証の場合は、ログイン画面を表示
        return redirect(route('login'));
    }
}

AuthUser の使用方法については、こちらをご覧ください。


セッション認証の設定

セッション認証のミドルウェアをアプリケーションに設置します。

/apricot/config

app.php
    'setup' =>[
        ...
    ],
    'middleware' =>[
        \App\Middleware\AccessLog::class,        /* Access log */
        \App\Middleware\VerifyCsrfToken::class,  /* Verify CSRF Token */
//      \App\Middleware\Auth\BasicAuth::class,   /* Basic Auth. */
        \App\Middleware\Auth\SessionAuth::class, /* Session Auth. */
     ],
    'csrf' =>[
        ...
    ],
    'auth' =>[
        'db'=>[
            'user'=>[
                'account' =>'account',
                'password' =>'password',
                'remember' =>'remember_token',
            ],
        ],
        'expires_sec'=> 2*7*24*3600, /* 2weekws */
        'menu'=> true,
    ],
];


認証コントローラ

ルーティング

以下のように config/routes.php を変更し、認証コントローラのルートを作ります。

/apricot/config

routes.php
//-------------------------------------------------------------------
// Route Definition Callback
//-------------------------------------------------------------------
return function (FastRoute\RouteCollector $r)
{
    $base = Core\Application::getInstance()->getRouteBase();
    $r->addGroup($base, function (FastRoute\RouteCollector $r) use($base)
    {
        // Auth
        $r->get ('/login', 'AuthController@showForm');
        $r->post('/login', 'AuthController@login');
        $r->get ('/logout', 'AuthController@logout');
 
        // User
        // ...
    });
};


AuthController クラス

以下に認証コントローラを示します。

/apricot/app/Controllers

AuthController.php
<?php
namespace App\Controllers;
 
use Core\Input;
use Core\Foundation\ErrorBag;
use App\Foundation\Security\AuthUser;
use App\Foundation\Controller;
use App\Foundation\ValidatorErrorBag;
 
/**
 * Authコントローラ
 */
class AuthController extends Controller
{
    /**
     * ログインフォーム表示
     * @return \Core\Foundation\Response
     */
    public function showForm()
    {
        if (AuthUser::check())
        {
            // 認証済ならトップ画面表示
            return redirect(route(''));
        }
 
        if (AuthUser::remember())
        {
            // 自動認証できたらトップ画面表示
            return redirect(route(''));
        }
 
        return render('login');
    }
 
    /**
     * ログイン(ユーザ認証)
     * @return \Core\Foundation\Response
     */
    public function login()
    {
        // バリデーション
        $response = $this->validate();
        if ($response instanceof \Core\Foundation\Response)
        {
            return $response;
        }
 
        $inputs = Input::all();
 
        if (!AuthUser::authenticate($inputs['account'], $inputs['password'], !empty($inputs['remember'])))
        {
            // ユーザが見つからない
            $errorBag = new ErrorBag([__('auth.login.error.no_account')]);
            return redirect(back())->withInputs()->withErrors($errorBag);
        }
 
        // ログイン成功
        return redirect(AuthUser::getPathAfterLogin());
    }
 
    /**
     * バリデーション
     * @return void|\Core\Foundation\Response return Response if failed
     */
    private function validate()
    {
        $inputs = Input::all();
 
        // Validation
        $v =(new \Valitron\Validator($inputs))
        ->rule('required', 'account')
        ->rule('alphaNum','account')
        ->rule('ascii','password')
        ->labels(inputLabels('auth.login'));
 
        if(!$v->validate())
        {
            $errorBag = new ValidatorErrorBag($v->errors());
            return redirect(back())->withInputs()->withErrors($errorBag);
        }
    }
 
    /**
     * ログアウト
     * @return \Core\Foundation\Response
     */
    public function logout()
    {
        // セッションの破棄
        AuthUser::forget();
 
        // ログイン画面表示
        return redirect(route("login"));
    }
}

AuthUser の使用方法については、こちらをご覧ください。

認証コントローラのバリデーションは(コードが少量なので)インターセプタークラスを作らずに自分のクラス内のメソッド( validate() で行っています。後述の「Apricot 拡張: インターセプター」では、validate()をクロージャにしてアクションと分離した形で再実装します。


ログイン画面

以下に、ログイン画面で使用するHTMLテンプレートを示します。

/apricot/assets/view

login.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
 
    <title>{{__('messages.app.title')}}</title>
 
    <!-- stylesheet -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link href="{{url_ver('css/main.css')}}" rel="stylesheet">
 
    <!-- Scripts -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
    <script src="{{url_ver('js/main.js')}}"></script>
    {!! DebugBar::renderHead() !!}
</head>
<body>
    <nav class="navbar navbar-dark bg-dark">
        <a class="navbar-brand" href="{{route()}}">{{__('messages.app.title')}}</a>
    </nav>
 
    <main class="container mt-sm-5 mt-1">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ __('auth.login.title') }}</div>
                    <div class="card-body">
                        <form method="POST" name="fm">
                            @csrf
 
                            {{-- account --}}
                            <div class="form-group row">
                                <label for="account" class="col-md-3 col-form-label text-md-right">{{__('auth.login.account')}}</label>
                                <div class="col-md-8">
                                    <input type="text" name="account" id="account" class="form-control" value="{{old('account')}}">
 
                                    @if($errors->has('account',ValidatorErrorBag::BAG_KEY))
                                    <span class="text-danger">{{$errors->get('account',ValidatorErrorBag::BAG_KEY)}}</span>
                                    @endif
                                </div>
                            </div>
 
                            {{-- password --}}
                            <div class="form-group row">
                                <label for="password" class="col-md-3 col-form-label text-md-right">{{__('auth.login.password')}}</label>
                                <div class="col-md-8">
                                    <input type="password" name="password" id="password" class="form-control">
 
                                    @if($errors->has('password',ValidatorErrorBag::BAG_KEY))
                                    <span class="text-danger">{{$errors->get('password',ValidatorErrorBag::BAG_KEY)}}</span>
                                    @endif
                                </div>
                            </div>
 
                            {{-- remember--}}
                            <div class="form-group row">
                                <div class="col-md-8 offset-md-3">
                                    <div class="form-check my-md-0 my-3">
                                        <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
                                        <label class="form-check-label" for="remember">{{ __('auth.login.remember') }}</label>
                                    </div>
                                </div>
                            </div>
 
                            {{-- button --}}
                            <div class="form-group row">
                                <div class="col-md-8 offset-md-3">
                                    <button type="submit" class="btn btn-primary" formaction="{{ route('login') }}">{{ __('auth.login.btn_login') }}</button>
                                </div>
                            </div>
                        </form>
                    </div>
 
                    @if($errors->count(ErrorBag::DEFAULT_NAME))
                    <div class="card-footer">
                        <div class="text-danger">
                            @foreach($errors->all(ErrorBag::DEFAULT_NAME) as $key=>$value)
                                 {{$value}}<br>
                            @endforeach
                        </div>
                    </div>
                    @endif
                </div>
            </div>
        </div>
 
    </main>
 
    <footer class="fixed-bottom bg-secondary text-center text-light py-1">&copy; 2020 y2sunlight</footer>
    {!! DebugBar::render() !!}
</body>
</html>


翻訳テキスト

ユーザ認証用の翻訳ファイル( auth.php )にログイン画面用の翻訳テキストを追加します。

apricot/assets/lang/ja

auth.php
<?php
return [
    'basic'=>[
       ...
    ],
    'login'=>[
        'title'=>'ログイン',
        'account'=>'アカウント',
        'password'=>'パスワード',
        'remember'=>'ログイン状態を保存する',
        'btn_login'=>'ログイン',
        'error'=>[
            'no_account'=>'アカウントが見つかりません'
        ],
    ],
];


その他の画面修正

layout.blade.php

アプリ作成の準備で作成したアプリ全体で使用するHTMLテンプレート layout.blade.phpを修正します。

/apricot/assets/view

layout.blade.php
...
            {{--
            @if(app('auth.menu',false))
            <ul class="navbar-nav ml-auto">
                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                    {{AuthUser::getUser()->account}} <span class="caret"></span>
                </a>
                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                    <a class="dropdown-item" href="{{route('logout')}}">{{__('messages.app.menu.logout')}}</a>
                </div>
            </ul>
            @endif
            --}}
...

ユーザメニュー

ユーザのアカウント名を表示し、そこをクリックするとユーザ用のメニューが表示されます。Apricotでは以下のユーザメニューが実装されています。必要に応じて、ここにユーザメニュー追加して下さい。

  • [ログアウト] — ログインセッションを終了します。


ホームコントローラー

ホーム画面にログインユーザ名を表示するようにホームコントローラーを修正します。

/apricot/app/Controllers

HomeController.php
<?php
namespace App\Controllers;
 
use App\Foundation\Controller;
use App\Foundation\Security\AuthUser;
 
/**
 * ホームコントローラ
 */
class HomeController extends Controller
{
    /**
     * Home Page
     * @return \Core\Foundation\Response
     */
    public function index()
    {
        $message = __('messages.home.msg_hello', [':account'=>AuthUser::getUser()->account]);
        return render('home',['message'=>$message]);
    }
}


テスト実行

セッション認証のテストをしてみましょう。初期状態(既定値)のユーザ名とパスワードは以下の通りです。初期のユーザ名とパスワードは idirom.setup.phpに設定されています。

ブラウザ上でホーム画面にアクセスすると:

http://localhost/ws2019/apricot/public/

ログイン画面が表示されます。

■ 正しいユーザ名とパスワードを入力して[ログイン]ボタンを押すと、ホーム画面が画面が表示されます。
■ [ログイン状態を保存する]をチェックすると、ログイン状態でブラウザを閉じてもログイン状態が保持され、再度Apricotにアクセスすると自動ログイン機能が働きます。自動ログインの有効期間はapp.phpのauth.expires_secで設定して下さい。

■ 画像の下にユーザのアカウントが表示されています。
■ ログアウトしたい場合は、画面右上のユーザ名をクリックして[ログアウト]を選択して下さい。