====== Apricot ユーザ認証 ====== --- //[[http://www.y2sunlight.com|y2sunlight]] 2020-05-15// [[apricot:top|Apricot に戻る]] 関連記事 * [[apricot:configuration|Apricot プロジェクトの作成]] * [[apricot:public|Apricot 公開フォルダ]] * [[apricot:core:top|Apricot コア]] * [[apricot:app:top|Apricot アプリ]] * Apricot 拡張 * [[apricot:ext:middleware|Apricot ミドルウェア]] * [[apricot:ext:access-log|Apricot アクセスログ]] * [[apricot:ext:csrf|Apricot CSRF対策]] * Apricot ユーザ認証 * [[apricot:ext:basic-auth|Apricot 基本認証]] * [[apricot:ext:session-auth|Apricot セッション認証]] * [[apricot:ext:interceptor|Apricot インターセプター]] * [[apricot:ext:di-container|Apricot DIコンテナー]] Apricotでは次の2つのユーザ認証をサポートしています。 * [[apricot:ext:basic-auth|基本認証]] --- HTTPで定義される[[https://ja.wikipedia.org/wiki/Basic%E8%AA%8D%E8%A8%BC|認証方法]] * [[apricot:ext:session-auth|セッション認証]] --- ログイン画面による認証で、ログイン状態をセッションで管理する方式 セッション認証では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インターフェースのコードを示します。 {{fa>folder-open-o}} ** /apricot/core/Foundation/Security ** \\ ==== 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クラスを以下に示します。 {{fa>folder-open-o}} ** /apricot/core/Foundation/Security ** 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インターフェースを実装しなければなりません。実装先はモデルクラス([[apricot:app:user-edit#ユーザモデル|User]])が相当と思われますが、モデルに直接コードを書くのではなくて、Authenticatableインターフェースのデフォルト実装をトレイト( AuthTrait )で作ります。こうしておくことで、どんなモデルにも容易にAuthentication インターフェースを実装することができます。 AuthTrait の使い方は以下の様です: /** * Authenticatableを実装するモデルクラス */ class FooModel implements Authenticatable { /** * Authenticatable User * Includeing default implementation of Authenticatable */ use AuthTrait; ... } \\ ==== AuthTraitトレイト ==== 本トレイトは[[apricot:app:db-model#モデルクラス|モデル]]のサブクラスから使用( ''use'' )を前提としています。また、AuthTraitではユーザ認証で使用するデータベース情報をアプリケーションの設定ファイル(app.php)から取得しています。app.phpによる設定方法については[[#AuthTraitの設定|後述]]します。 以下に AuthTrait を示します。 {{fa>folder-open-o}} ** /apricot/app/Foundation/Security ** 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")]]); } } * 全体的には、[[basic-library:idiorm:1.5|Idiorm]]のORMクラスを使った実装になっており見ての通りです。 * GetAuthName()の実装で使っている ''$this->tableName()'' は[[apricot:app:db-model#モデルクラス|モデルクラス( Model )]]のメソッドです。 \\ ==== AuthTraitの設定 ==== AuthTrait で使用するデータベース情報の設定は、アプリケーションの設定ファイル(app.php)で行います。 {{fa>folder-open-o}} ** /apricot/config** [ ... ], 'middleware' =>[ ... ], 'csrf' =>[ ... ], 'auth' =>[ 'db'=>[ 'user'=>[ 'account' =>'account', 'password' =>'password', 'remember' =>'remember_token', ], ], ], ]; * auth.db--- ユーザ認証で使用するデータベース情報 * user --- テーブル名 * account --- アカウントのカラム名 * password --- パスワードのカラム名 * remember --- RememberMeトークンのカラム名 auth.db の設定はユーザ認証で使用するデータベース(テーブル名とそのカラム)に関する設定です。上記ではテーブル名として user を設定していますが、必要に応じて他のテーブルにすることもできます。 \\ ===== ユーザ認証クラス ===== ここまでで、ユーザ認証の準備は終わりました。残りの作業は: - ユーザモデル( [[apricot:app:user-edit#ユーザモデル|User]] )に[[#AuthTraitトレイト|AuthTraitトレイト]]を使って[[#Authenticatableインターフェース|Authenticatableインターフェース]]を実装する - ユーザモデル( [[apricot:app:user-edit#ユーザモデル|User]] )を使って、[[#Authenticationクラス|Authenticationクラス]]のシングルトンであるユーザ認証クラス( AuthUser )を生成する 以下で順に説明していきます。 ==== ユーザモデル ==== ユーザモデル( User )に[[#Authenticatableインターフェース|Authenticatableインターフェース]]を実装します。Authenticatableの実装は、AuthTraitを使用( ''use'' )するだけですが、[[#AuthTraitの設定|AuthTraitの設定]]が必要になるので忘れないで下さい。 {{fa>folder-open-o}} ** /apricot/app/Model ** \\ ==== AuthUserクラス ==== AuthUserクラスは、コアの [[#authenticationクラス|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パスを取得する。| {{fa>folder-open-o}} ** /apricot/core/Foundation/Security ** * createInstance() では、ユーザモデル( User )を使って、Authenticationクラスのシングルトンを生成しています。 === マルチ認証 === 同様の方法で別のモデルを使用したAuthenticationクラスのシングルトンも同時に作る事ができます。例えば、Userとは別に、AdminUserクラスやApiUserクラスも認証したいなどの例には実際によく遭遇するかもしれません。このように、Apricotでは潜在的にマルチ認証を想定した実装に仕上げていますが、これはApricotの範囲を超えているので、これ以上は触れないことにします。 \\ ==== クラスエイリアス ==== AuthUserクラスを[[apricot:app:top#クラスエイリアス|クラスエイリアス]]に追加します。 {{fa>folder-open-o}} ** /apricot/config/setup ** \App\Helpers\ViewHelper::class, 'ValidatorErrorBag' => \App\Foundation\ValidatorErrorBag::class, 'AuthUser' => \App\Foundation\Security\AuthUser::class, ]; .... }; * ValidatorErrorBag の下に AuthUserのエイリアスを追加します。 \\