はじめに
お仕事で、Zend Frameworkのバージョンアップをしなければならなくなった・・と思ったら、Zend Frameworkはもうなくて、Laminas Projectに移って新たなフレームワークとして公開されている。
いちから勉強しないといけないじゃん、ということで、必要な要件を満たせるかどうかを一歩ずつ調査していく。
要件(8)
8つ目の要件は、「カスタマイズされたユーザー認証手順での認証ができること」。
Zend Frameworkで言うところのZend_Auth
を使った独自認証機構。
渡されたユーザー名からテナントDBを特定して、(アルゴリズムは一般的だけど)独自のパスワードハッシュ関数を使って渡されたパスワードをハッシュ化して照合して、ログイン認証に成功したらセッションにユーザー情報を保持しておく、というような流れを一通り作っていく。
導入
こちらでセットアップした環境を(コピーして)使っていく。
私は以下のようにコピーを作成。
cp -pr laminas-setup-7-session-data laminas-setup-8-user-auth cd laminas-setup-8-user-auth
検証方針
すべてを真面目に組み上げると、パスワードのハッシュ関数なども実装しなければならなくなるので、ここでは流れだけを確認するために、以下のようなものを実装することにする。
- 認証時には以下のパラメータを渡す
- GETパラメータでテナントコード(tenant_code)を渡す
- GETパラメータでユーザー名(user_name)を渡す
- 渡されたテナントコードを元にテナントDBを特定する。特定できない場合は認証エラー
- 渡されたユーザー名を元に、特定されたテナントDB内に該当するユーザー名が存在するかを確認する。存在しなければ認証エラー
- 特定されたユーザーアカウントの識別子をセッション情報に保持する
- 検証用ページでは、以下のような表示をする
- ユーザー識別子がセッションになければ「未ログイン」と表示する
- ユーザー識別子がセッションに保存されていたら、テナントDBから取得できる実名(real_name)とともに「ログイン済み」と表示する
設定・実装
まず、コントローラmodule/Application/src/Controller/UserAuthController.php
を作っていく。
<?php declare(strict_types=1); namespace Application\Controller; use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; use Laminas\Session\Container; use Application\Model\DbMaster2Table; use Application\Model\Tenant\UserMasterTable; use Application\Model\Tenant\MessagesTable; class UserAuthController extends AbstractActionController { private $userMasterTable; private $securityAuth; public function __construct(UserMasterTable $userMasterTable = null) { $this->userMasterTable = $userMasterTable; } public function indexAction() { $container = new Container('user'); if (isset($container->userId)) { $user = $this->userMasterTable->getUser($container->userId); $message = 'ログイン済み:' . $user->realName; } else { $message = '未ログイン'; } return new ViewModel([ 'message' => $message, ]); } public function loginAction() { $userName = $_GET['user_name']; $user = $this->userMasterTable->getUserByName($userName); $container = new Container('user'); $container->tenantCode = $_GET['tenant_code']; $container->userId = $user->id; return $this->redirect()->toRoute('user-auth', ['action' => 'index']); } public function logoutAction() { $container = new Container('user'); $container->getManager()->getStorage()->clear(); return $this->redirect()->toRoute('user-auth', ['action' => 'index']); } }
続いて、module/Application/config/module.config.php
のrouter
設定に以下の内容を追加する。
<?php : 'user-auth' => [ 'type' => Segment::class, 'options' => [ 'route' => '/user-auth[/:action]', 'defaults' => [ 'controller' => Controller\UserAuthController::class, 'action' => 'index', ], ], ],
さらに、module/Application/src/Module.php
を以下のように変更する。
<?php declare(strict_types=1); namespace Application; use Laminas\Db\Adapter\Adapter; use Laminas\Db\Adapter\AdapterInterface; use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\TableGateway\TableGateway; use Laminas\ModuleManager\Feature\ConfigProviderInterface; use Laminas\Session\Container; class Module implements ConfigProviderInterface { public function getConfig(): array { /** @var array $config */ $config = include __DIR__ . '/../config/module.config.php'; return $config; } public function getServiceConfig(): array { return [ 'factories' => [ Model\DbMaster2Table::class => function($container) { $tableGateway = $container->get(Model\DbMaster2TableGateway::class); return new Model\DbMaster2Table($tableGateway); }, Model\DbMaster2TableGateway::class => function($container) { $dbAdapter = $container->get(AdapterInterface::class); $resultSetPrototype = new ResultSet(); $resultSetPrototype->setArrayObjectPrototype(new Model\DbMaster2()); return new TableGateway('db_master_2', $dbAdapter, null, $resultSetPrototype); }, Model\Tenant\UserMasterTable::class => function($container) { $tableGateway = $container->get(Model\Tenant\UserMasterTableGateway::class); if (!is_null($tableGateway)) { return new Model\Tenant\UserMasterTable($tableGateway); } else { return null; } }, Model\Tenant\UserMasterTableGateway::class => function($container) { $dbAdapter = $container->get(Model\Tenant\Adapter::class); if (!is_null($dbAdapter)) { $resultSetPrototype = new ResultSet(); $resultSetPrototype->setArrayObjectPrototype(new Model\Tenant\UserMaster()); return new TableGateway('user_master', $dbAdapter, null, $resultSetPrototype); } else { return null; } }, Model\Tenant\MessagesTable::class => function($container) { $dbAdapter = $container->get(Model\Tenant\Adapter::class); return new Model\Tenant\MessagesTable($dbAdapter); }, Model\Tenant\Adapter::class => function($container) { $userContainer = new Container('user'); $tenantCode = $userContainer->tenantCode; if (is_null($tenantCode)) { $tenantCode = $_GET['tenant_code']; } if (!is_null($tenantCode)) { $dbMaster2Table = $container->get(Model\DbMaster2Table::class); $result = $dbMaster2Table->getDb($tenantCode); $config = [ 'driver' => 'pdo_pgsql', 'database' => $result->dbName, 'username' => $result->dbUser, 'password' => $result->dbPassword, 'hostname' => $result->dbHost, 'port' => $result->dbPort, 'charset' => 'UTF-8', ]; $dbAdapter = new Adapter($config); return $dbAdapter; } else { return null; } }, ], ]; } public function getControllerConfig() { return [ 'factories' => [ Controller\TenantController::class => function($container) { return new Controller\TenantController( $container->get(Model\DbMaster2Table::class), $container->get(Model\Tenant\UserMasterTable::class), $container->get(Model\Tenant\MessagesTable::class) ); }, Controller\UserAuthController::class => function($container) { return new Controller\UserAuthController( $container->get(Model\Tenant\UserMasterTable::class) ); }, ], ]; } }
続いて、module/Application/src/Model/Tenant/UserMasterTable.php
に新規メソッドgetUserByName()
を追加する。
<?php :(中略) public function getUserByName($userName) { if (is_null($userName)) { throw new RuntimeException('User name is required.'); } $rowset = $this->tableGateway->select(['user_name' => $userName]); $row = $rowset->current(); if (!$row) { throw new RuntimeException(sprintf( 'Could not find row with user name %s', $userName )); } return $row; } }
最後に、ビューテンプレートmodule/Application/view/application/user-auth/index.tpl
を以下の内容で作成する。
{$message|escape:"html"}
動作確認
以下のURLにアクセスする。
http://192.168.56.xxx/laminas-setup-8-user-auth/public/user-auth/index
最初は以下のような画面が表示されればOK。
次に、以下のURLにアクセスする。
以下のような画面が表示されればOK。
まとめ
- Zend_Authに相当するものはなくなっているようなので、通常のセッションデータ保持の仕組みを使って自前で書く形になる
- テーブル名やカラム名を渡すと、ユーザー名やパスワードの照合をよしなにしてくれるクラスも提供されているようだけど、今回の要件には合わないので割愛した