— y2sunlight 2020-05-15
関連記事
ミドルウェアを使ってユーザのセッション認証を実装します。本章ではApricotのユーザ認証機能を使用しているので、まだお読みでない方は先にそちらをご一読下さい。
セッション認証ではログイン画面を作成してユーザ認証を行います。この認証方法ではログアウト機能が有効で、Remember-Meトークンによる自動ログイン機能も実装します。比較的多くのユーザでサイトを運用する場合はこの認証方法をお薦めします。
本機能は次の2つの部分に分かれます。
以下に、セッション認証のミドルウェアを示します。
/apricot/app/Middleware/Auth
<?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
'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
//------------------------------------------------------------------- // 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 // ... }); };
以下に認証コントローラを示します。
/apricot/app/Controllers
<?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
<!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">© 2020 y2sunlight</footer> {!! DebugBar::render() !!} </body> </html>
ユーザ認証用の翻訳ファイル( auth.php )にログイン画面用の翻訳テキストを追加します。
apricot/assets/lang/ja
<?php return [ 'basic'=>[ ... ], 'login'=>[ 'title'=>'ログイン', 'account'=>'アカウント', 'password'=>'パスワード', 'remember'=>'ログイン状態を保存する', 'btn_login'=>'ログイン', 'error'=>[ 'no_account'=>'アカウントが見つかりません' ], ], ];
アプリ作成の準備で作成したアプリ全体で使用するHTMLテンプレート layout.blade.phpを修正します。
/apricot/assets/view
... {{-- @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 --}} ...
<nav>
の中の {{--
と --}}
に囲まれたコメント部分を解除して下さい。ユーザのアカウント名を表示し、そこをクリックするとユーザ用のメニューが表示されます。Apricotでは以下のユーザメニューが実装されています。必要に応じて、ここにユーザメニュー追加して下さい。
ホーム画面にログインユーザ名を表示するようにホームコントローラーを修正します。
/apricot/app/Controllers
<?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]); } }
AuthUser
の use
を追加します。 [':account'=>env('APP_NAME')]
の部分を AuthUser::getUser()->account
に変更します。
セッション認証のテストをしてみましょう。初期状態(既定値)のユーザ名とパスワードは以下の通りです。初期のユーザ名とパスワードは idirom.setup.phpに設定されています。
'root'
''
※空文字ブラウザ上でホーム画面にアクセスすると:
http://localhost/ws2019/apricot/public/
ログイン画面が表示されます。
■ 正しいユーザ名とパスワードを入力して[ログイン]ボタンを押すと、ホーム画面が画面が表示されます。
■ [ログイン状態を保存する]をチェックすると、ログイン状態でブラウザを閉じてもログイン状態が保持され、再度Apricotにアクセスすると自動ログイン機能が働きます。自動ログインの有効期間はapp.phpのauth.expires_secで設定して下さい。
■ 画像の下にユーザのアカウントが表示されています。
■ ログアウトしたい場合は、画面右上のユーザ名をクリックして[ログアウト]を選択して下さい。