メインメニュー
XAMPP アレンジ
IED
WSL2
-
道具箱
リポジトリ編
フレームワーク編
公開ソフトウェア
メタ
リンク
- PHP ライブラリ
- PHP 言語
apricot:ext:user-auth目次
Apricot ユーザ認証
— y2sunlight 2020-05-15
関連記事
- Apricot 拡張
- Apricot ユーザ認証
Apricotでは次の2つのユーザ認証をサポートしています。
- セッション認証 — ログイン画面による認証で、ログイン状態をセッションで管理する方式
セッション認証ではRemember-Meトークンによる自動ログインもサポートしています。これらのユーザ認証の実装は次章以降で行います。
本章の目的は、これらのユーザ認証の基盤を作る事です。その着地点は、シングルトンで動作するユーザ認証クラス( AuthUser )の実装です。このクラスを使って、基本認証またはセッション認証の機能を次章以降で作って行きます。
コアのユーザ認証機能
Apricotのコアで提供するユーザ認証機能は、アプリケーションのモデルやデータベースに依存しない基本的なものだけです。その代わりに、アプリ側とのインターフェース(Authenticatable)を提供します。Authenticatable を使用することで、モデルやデータベースに依存しないユーザ認証機能の基盤を実装することができます。
Authenticatableインターフェース
ユーザ認証インターフェース(Authenticatable)を定義します。
メソッド 機能 getAuthName():string 認証を識別できる任意の名前を返します。
通常は認証のモデルとなるテーブル名を返します。(例:user)authenticateUser
(string $account, string $password)
:object|boolユーザの認証を行います。
成功の場合ユーザオブジェクトを返し、他は false を返します。
(アカウントとパスワードによるログイン画面で使用)remember
(string $remenber_token)
:object|boolユーザのRemnber-Meトークンによる認証を行います。
成功の場合ユーザオブジェクトを返し、他は false を返します。
(RememberMeトークンによる自動ログインで使用)retrieveUser(object $user)
:object|boolユーザオブジェクトの再検索を行います。
成功の場合ユーザオブジェクトを返し、他は false を返します。
再検索のキーは引数 $user のアカウントが使えます。saveRemenberToken
(object $user, string $remenber_token)
:boolユーザのRemnber-Meトークンを保存します。
保存時のキーは引数 $user のアカウントが使えます。以下に、Authenticatableインターフェースのコードを示します。
/apricot/core/Foundation/Security
- Authenticatable.php
<?php namespace Core\Foundation\Security; /** * Authenticatable interface */ interface Authenticatable { /** * Get authentication name * @return string */ public function getAuthName():string; /** * Authenticate user * @param string $account * @param string $password * @return object|bool return object if authenticated, else return false */ public function authenticateUser(string $account, string $password); /** * Remember user * @param string $remenber_token * @return object|bool return object if authenticated, else return false */ public function rememberUser(string $remenber_token); /** * Retrieve user * @param object $user * @return object|bool return object if success, else return false */ public function retrieveUser(object $user); /** * Save remember token * @param object $user * @return bool|bool return true if success, else return false */ public function saveRemenberToken(object $user, string $remenber_token):bool; }
Authenticationクラス
Authenticatableインターフェースを使ったユーザ認証基本クラス(Authentication)を作ります。Authenticationは以下の公開メソッドを持ちます。
メソッド 機能 __construct
(Authenticatable $auth)Authenticationのコンストラクタ
生成時にAuthenticatableインターフェースを与えます。authenticate
(string $account, string $password,
bool $remenber=false):boolユーザの認証を行い、成功の場合trueを返します。
(アカウントとパスワードによるログイン画面で使用)remember():bool ユーザの自動認証を行い、成功の場合trueを返します。
(RememberMeトークンによる自動ログインで使用)check():bool ユーザが認証されているか否かを返す。 verify():bool ユーザが認証されているか否かを返す。
認証されている場合、ログインユーザ情報の更新を行います。forget() ログインセッションを削除する。 getUser():object ログインユーザを取得する。 getPathAfterLogin():string ログイン後のURLパスを取得する。 本クラスは基本的にセッション認証用に作られていますが、基本認証の場合も authenticate()、verify()、getUser()メソッドは使用できます(基本認証の認証画面はブラウザによって提供され、ログアウトという考え方もなく、セッションとログイン状態は常に一致します)。また、check() と verify() は似たようなメソッドですが、check()はアクションで、verify()はミドルウェアでの使用を想定しています。
Authenticationクラスを以下に示します。
/apricot/core/Foundation/Security
- Authentication.php
<?php namespace Core\Foundation\Security; use Core\Cookie; use \Core\Session; /** * Authentication */ class Authentication { /** * @var integer */ private const TOKEN_LENGTH = 64; /** * Session key for authenticated user */ private const SESSION_KEY_AUTH = '_auth_'; /** * Session key for path after login */ private const SESSION_KEY_PATH_AFTER_LOGIN = '_path_after_login_'; /** * Cookie key for remembered user */ private const COOKIE_KEY_REMEMBER = '_remember_'; /** * Authentication name * @var string */ private $name; /** * Authentication interface * @var Authenticatable */ private $auth; /** * Create authentication object * @param string $name */ public function __construct(Authenticatable $auth) { $this->auth = $auth; $this->name = $this->auth->getAuthName(); } /** * Authenticate user (Login) * @param string $account * @param string $password * @param bool $remenber * @return bool true if authenticated */ public function authenticate(string $account, string $password, bool $remenber=false): bool { $user = $this->auth->authenticateUser($account, $password, $remenber); if ($user!== false) { //Set user session $this->setUserSession($user, $remenber); return true; } return false; } /** * Remember user (Auto Login) * @return bool true if authenticated */ public function remember() { if(Cookie::has($this->getRemenberCookieName())) { $user = $this->auth->rememberUser(Cookie::get($this->getRemenberCookieName())); if (($user!==false)) { // Set user session $this->setUserSession($user, true); return true; } } return false; } /** * Returns whether the user has been authenticated * @return bool true if authenticated */ public function check(): bool { return Session::has(self::SESSION_KEY_AUTH.$this->name); } /** * Verify whether user is authenticated * @return bool true if authenticated */ public function verify(): bool { // When alraedy logged in if ($this->check()) { $user = $this->getUser(); // Retrieve login user info $new_user_info = $this->auth->retrieveUser($user); // The login user may have been deleted, but keep on login if ($new_user_info!==false) { $this->setUser($new_user_info); } return true; } // If not authenticated, remember the path after login $this->setPathAfterLogin($_SERVER['REQUEST_URI']); return false; } /** * Forget user's session and cookie */ public function forget() { // Destroy session completely Session::destroy(); // Remove user from cookie Cookie::remove($this->getRemenberCookieName()); } /** * Get authenticated user * @return object */ public function getUser() { return Session::get(self::SESSION_KEY_AUTH.$this->name); } /** * Get path after login * @return string */ public function getPathAfterLogin() : string { return Session::get(self::SESSION_KEY_PATH_AFTER_LOGIN.$this->name, route('')); } /** * Set user session * @param object $user * @param bool $remenber */ private function setUserSession(object $user, bool $remenber) { // Save user in session $this->setUser($user); if ($remenber) { $remenber_token = $this->getRemenberToken(); // Save remenber_token to DB if ($this->auth->saveRemenberToken($user, $remenber_token)) { // Save to cookie Cookie::set($this->getRemenberCookieName(), $remenber_token, app('auth.expires_sec')); } } else { // Remove from cookie Cookie::remove($this->getRemenberCookieName()); } } /** * Get login user * @return object */ private function setUser(object $user) { return Session::set(self::SESSION_KEY_AUTH.$this->name, $user); } /** * Set path after login * @param string $path */ private function setPathAfterLogin(string $path) { return Session::set(self::SESSION_KEY_PATH_AFTER_LOGIN.$this->name, $path); } /** * Get remenber me cookie name * @return string */ private function getRemenberCookieName():string { return self::COOKIE_KEY_REMEMBER.$this->name.'_'.sha1(env('APP_SECRET',self::class)); } /** * Get remenber me token * @return string */ private function getRemenberToken():string { return str_random(self::TOKEN_LENGTH); } }
アプリ側のユーザ認証共通機能
コアのユーザ認証基本クラス(Authentication)を使用する為には、Authenticatableインターフェースを実装しなければなりません。実装先はモデルクラス(User)が相当と思われますが、モデルに直接コードを書くのではなくて、Authenticatableインターフェースのデフォルト実装をトレイト( AuthTrait )で作ります。こうしておくことで、どんなモデルにも容易にAuthentication インターフェースを実装することができます。
AuthTrait の使い方は以下の様です:
/** * Authenticatableを実装するモデルクラス */ class FooModel implements Authenticatable { /** * Authenticatable User * Includeing default implementation of Authenticatable */ use AuthTrait; ... }
AuthTraitトレイト
本トレイトはモデルのサブクラスから使用(
use
)を前提としています。また、AuthTraitではユーザ認証で使用するデータベース情報をアプリケーションの設定ファイル(app.php)から取得しています。app.phpによる設定方法については後述します。以下に AuthTrait を示します。
/apricot/app/Foundation/Security
- AuthTrait.php
<?php namespace App\Foundation\Security; use ORM; use Core\Log; /** * Authenticatable User * Includeing default implementation of Authenticatable */ trait AuthTrait { /** * Get authentication name * {@inheritDoc} * @see \Core\Foundation\Security\Authenticatable::getAuthName() */ public function getAuthName(): string { return $this->tableName(); } /** * Authenticate user * {@inheritDoc} * @see \Core\Foundation\Security\Authenticatable::authenticateUser() */ public function authenticateUser(string $account, string $password) { $table = $this->getAuthName(); $user = ORM::for_table($table) ->where([app("auth.db.{$table}.account")=>$account]) ->find_one(); if (($user!==false) && (password_verify($password, $user->as_array()[app("auth.db.{$table}.password")]))) { Log::notice("authenticate",[$account]); return $user; } return false; } /** * Remember user * {@inheritDoc} * @see \Core\Foundation\Security\Authenticatable::rememberUser() */ public function rememberUser(string $remenber_token) { $table = $this->getAuthName(); $user = ORM::for_table($table) ->where([app("auth.db.{$table}.remember")=>$remenber_token]) ->find_one(); if (($user!==false)) { Log::notice("remember",[$user->as_array()[app("auth.db.{$table}.account")]]); return $user; } return false; } /** * Retrieve user * {@inheritDoc} * @see \Core\Foundation\Security\Authenticatable::retrieveUser() */ public function retrieveUser(object $user) { $table = $this->getAuthName(); $new_user = ORM::for_table($table) ->where('account',$user->as_array()[app("auth.db.{$table}.account")]) ->find_one(); return $new_user; } /** * Save remenber token * {@inheritDoc} * @see \Core\Foundation\Security\Authenticatable::saveRemenberToken() */ public function saveRemenberToken(object $user, string $remenber_token): bool { $table = $this->getAuthName(); $pdo = ORM::get_db(); $sql = "update ".$table. " set ".app("auth.db.{$table}.remember")."=?". " where ".app("auth.db.{$table}.account")."=?"; $stmt = $pdo->prepare($sql); return $stmt->execute([$remenber_token, $user->as_array()[app("auth.db.{$table}.account")]]); } }
- 全体的には、IdiormのORMクラスを使った実装になっており見ての通りです。
- GetAuthName()の実装で使っている
$this→tableName()
はモデルクラス( Model )のメソッドです。
AuthTraitの設定
AuthTrait で使用するデータベース情報の設定は、アプリケーションの設定ファイル(app.php)で行います。
/apricot/config
- app.php
<?php return [ 'setup' =>[ ... ], 'middleware' =>[ ... ], 'csrf' =>[ ... ], 'auth' =>[ 'db'=>[ 'user'=>[ 'account' =>'account', 'password' =>'password', 'remember' =>'remember_token', ], ], ], ];
- auth.db— ユーザ認証で使用するデータベース情報
- user — テーブル名
- account — アカウントのカラム名
- password — パスワードのカラム名
- remember — RememberMeトークンのカラム名
auth.db の設定はユーザ認証で使用するデータベース(テーブル名とそのカラム)に関する設定です。上記ではテーブル名として user を設定していますが、必要に応じて他のテーブルにすることもできます。
ユーザ認証クラス
ここまでで、ユーザ認証の準備は終わりました。残りの作業は:
- ユーザモデル( User )を使って、Authenticationクラスのシングルトンであるユーザ認証クラス( AuthUser )を生成する
以下で順に説明していきます。
ユーザモデル
ユーザモデル( User )にAuthenticatableインターフェースを実装します。Authenticatableの実装は、AuthTraitを使用(
use
)するだけですが、AuthTraitの設定が必要になるので忘れないで下さい。/apricot/app/Model
- User.php
<?php namespace App\Models; use App\Foundation\Model; use ORM; use Core\Foundation\Security\Authenticatable; use App\Foundation\Security\AuthTrait; /** * ユーザモデル */ class User extends Model implements Authenticatable { /** * Authenticatable User * Includeing default implementation of Authenticatable */ use AuthTrait; ... }
AuthUserクラス
AuthUserクラスは、コアの Authentication クラスをシングルトンにしたもので、Authenticationのメソッドが全て使用できます。AuthUserクラスは、主にミドルウェアと認証コントローラによって使用されますが、getUser()メソッドは様々な所から呼び出される可能性があります。
使用法: AuthUser::{メソッド}
メソッド 機能 __construct
(Authenticatable $auth)Authenticationのコンストラクタ
生成時にAuthenticatableインターフェースを与えます。authenticate
(string $account, string $password,
bool $remenber=false):boolユーザの認証を行い、成功の場合trueを返します。
(アカウントとパスワードによるログイン画面で使用)remember():bool ユーザの自動認証を行い、成功の場合trueを返します。
(RememberMeトークンによる自動ログインで使用)check():bool ユーザが認証されているか否かを返す。 verify():bool ユーザが認証されているか否かを返す。
認証されている場合、ログインユーザ情報の更新を行います。forget() ログインセッションを削除する。 getUser():object ログインユーザを取得する。 getPathAfterLogin():string ログイン後のURLパスを取得する。 /apricot/core/Foundation/Security
- AuthUser.php
<?php namespace App\Foundation\Security; use Core\Foundation\Singleton; use Core\Foundation\Security\Authentication; use App\Models\User; /** * User Authentication * * @method static Authentication getInstance(); * @method static bool authenticate(string $account, string $password, bool $remenber=false) Authenticate user (Login) * @method static void remember() Remember user (Auto Login) * @method static bool check() Returns whether the user has been authenticated * @method static bool verify() Verify whether user is authenticated * @method static void forget() Forget user's session and cookie * @method static object getUser() Get authenticated user * @method static string getPathAfterLogin() Get path after login */ class AuthUser extends Singleton { /** * Create user authentication instance. * @return \Core\Foundation\Security\Authentication */ protected static function createInstance() { return new Authentication(new User()); } }
- createInstance() では、ユーザモデル( User )を使って、Authenticationクラスのシングルトンを生成しています。
マルチ認証
同様の方法で別のモデルを使用したAuthenticationクラスのシングルトンも同時に作る事ができます。例えば、Userとは別に、AdminUserクラスやApiUserクラスも認証したいなどの例には実際によく遭遇するかもしれません。このように、Apricotでは潜在的にマルチ認証を想定した実装に仕上げていますが、これはApricotの範囲を超えているので、これ以上は触れないことにします。
クラスエイリアス
AuthUserクラスをクラスエイリアスに追加します。
/apricot/config/setup
- aliases.setup.php
<?php //------------------------------------------------------------------- // ビューテンプレートで使うクラスエイリアスを登録 //------------------------------------------------------------------- return function():bool { $aliases = [ /* Core */ .... /* App */ 'ViewHelper' => \App\Helpers\ViewHelper::class, 'ValidatorErrorBag' => \App\Foundation\ValidatorErrorBag::class, 'AuthUser' => \App\Foundation\Security\AuthUser::class, ]; .... };
- ValidatorErrorBag の下に AuthUserのエイリアスを追加します。
apricot/ext/user-auth.txt · 最終更新: 2020/06/08 11:38 by tanaka
コメント