— y2sunlight 2020-05-15
関連記事
Apricotでは次の2つのユーザ認証をサポートしています。
セッション認証ではRemember-Meトークンによる自動ログインもサポートしています。これらのユーザ認証の実装は次章以降で行います。
本章の目的は、これらのユーザ認証の基盤を作る事です。その着地点は、シングルトンで動作するユーザ認証クラス( AuthUser )の実装です。このクラスを使って、基本認証またはセッション認証の機能を次章以降で作って行きます。
Apricotのコアで提供するユーザ認証機能は、アプリケーションのモデルやデータベースに依存しない基本的なものだけです。その代わりに、アプリ側とのインターフェース(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
<?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; }
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
<?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; ... }
本トレイトはモデルのサブクラスから使用( use
)を前提としています。また、AuthTraitではユーザ認証で使用するデータベース情報をアプリケーションの設定ファイル(app.php)から取得しています。app.phpによる設定方法については後述します。
以下に AuthTrait を示します。
/apricot/app/Foundation/Security
<?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")]]); } }
$this→tableName()
はモデルクラス( Model )のメソッドです。
AuthTrait で使用するデータベース情報の設定は、アプリケーションの設定ファイル(app.php)で行います。
/apricot/config
<?php return [ 'setup' =>[ ... ], 'middleware' =>[ ... ], 'csrf' =>[ ... ], 'auth' =>[ 'db'=>[ 'user'=>[ 'account' =>'account', 'password' =>'password', 'remember' =>'remember_token', ], ], ], ];
auth.db の設定はユーザ認証で使用するデータベース(テーブル名とそのカラム)に関する設定です。上記ではテーブル名として user を設定していますが、必要に応じて他のテーブルにすることもできます。
ここまでで、ユーザ認証の準備は終わりました。残りの作業は:
以下で順に説明していきます。
ユーザモデル( User )にAuthenticatableインターフェースを実装します。Authenticatableの実装は、AuthTraitを使用( use
)するだけですが、AuthTraitの設定が必要になるので忘れないで下さい。
/apricot/app/Model
<?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クラスは、コアの 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
<?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()); } }
同様の方法で別のモデルを使用したAuthenticationクラスのシングルトンも同時に作る事ができます。例えば、Userとは別に、AdminUserクラスやApiUserクラスも認証したいなどの例には実際によく遭遇するかもしれません。このように、Apricotでは潜在的にマルチ認証を想定した実装に仕上げていますが、これはApricotの範囲を超えているので、これ以上は触れないことにします。
AuthUserクラスをクラスエイリアスに追加します。
/apricot/config/setup
<?php //------------------------------------------------------------------- // ビューテンプレートで使うクラスエイリアスを登録 //------------------------------------------------------------------- return function():bool { $aliases = [ /* Core */ .... /* App */ 'ViewHelper' => \App\Helpers\ViewHelper::class, 'ValidatorErrorBag' => \App\Foundation\ValidatorErrorBag::class, 'AuthUser' => \App\Foundation\Security\AuthUser::class, ]; .... };