HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

Zend FrameworkからLaminasに移行する話(8)

はじめに

お仕事で、Zend Frameworkのバージョンアップをしなければならなくなった・・と思ったら、Zend Frameworkはもうなくて、Laminas Projectに移って新たなフレームワークとして公開されている。

いちから勉強しないといけないじゃん、ということで、必要な要件を満たせるかどうかを一歩ずつ調査していく。

要件(8)

8つ目の要件は、「カスタマイズされたユーザー認証手順での認証ができること」。

Zend Frameworkで言うところのZend_Authを使った独自認証機構。 渡されたユーザー名からテナントDBを特定して、(アルゴリズムは一般的だけど)独自のパスワードハッシュ関数を使って渡されたパスワードをハッシュ化して照合して、ログイン認証に成功したらセッションにユーザー情報を保持しておく、というような流れを一通り作っていく。

導入

こちらでセットアップした環境を(コピーして)使っていく。

hhelibex.hatenablog.jp

私は以下のようにコピーを作成。

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.phprouter設定に以下の内容を追加する。

<?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。

laminas-setup-8-user-auth-1

次に、以下のURLにアクセスする。

http://192.168.56.xxx/laminas-setup-8-user-auth/public/user-auth/login?tenant_code=0001&user_name=tenant0001

以下のような画面が表示されればOK。

laminas-setup-8-user-auth-2

まとめ

  • Zend_Authに相当するものはなくなっているようなので、通常のセッションデータ保持の仕組みを使って自前で書く形になる
  • テーブル名やカラム名を渡すと、ユーザー名やパスワードの照合をよしなにしてくれるクラスも提供されているようだけど、今回の要件には合わないので割愛した