もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

laravelのセッションを別プロジェクトに持っていく

はじめに

こんばんは。 今回も備忘録ブログです。 最近多いな。備忘録。

現在お仕事もらっている案件で、独自フレームワークを使用しているPHPプロジェクトをLaravelに載せ替えるという案件をやっています。

理由としてはよくある感じで、その独自フレームワークを保守できる人が居なくなっており、コア部分もバンバンnoticeでているみたいな状態だからのようです。

すでにそのプロジェクトのとあるディレクトリにLaravelプロジェクトが入っており、Sessionを使わないところ以外は部分的に載せ替えられているが、Sessionを使用する部分では、当然ですがLaravel側の求めるSessionの形ではないので、Sessionの共存が出来ないようでした。

今回無理やりですが、共存できるようにSessionHandlerを作成して、そのクラスを session_set_save_handler() で指定して使用するようにしました。

前提として、SessionはDBを使用しています。

やったこと

1. 旧システムにlaravelをinstall

本当は Illuminate の該当部分のみでよいのかもしれませんが、とりあえず全部入れてみました。。 この辺適当です。すいません。

$ composer require laravel/laravel

2. laravel用sessionテーブル作成

artisanのコマンドでsessionテーブルのマイグレーションファイルを作成して、実行します。

$ php artisan session:table 
$ php artisan migrate

3. laravel側session設定

config/session.php を設定します。 該当箇所は driver cookie の2つです。 実際は、.envに以下を追加するという修正です。

SESSION_DRIVER=database
SESSION_COOKIE=xxxxxxxxxxxxxxxx

4. laravel側session設定を旧プロジェクトに持っていく

config/session.php の各設定を旧プロジェクトに定数として持っていきます。 以下はlaravel5.8のデフォルト設定です。

define("SESSION_NAME", "xxxxxxxxxxxxxxxxxxxxx");
define("SESSION_TIME", time() + (120 * 60));
define("SESSION_PATH", "/");
define("SESSION_DOMAIN", null);
define("SESSION_SECURE", false);
define("SESSION_HTTP_ONLY", true);

5. application keyと、cipherの設定を旧プロジェクトにコピーして持っていく

laravelプロジェクトを作成する際に 以下のコマンドでアプリケーションのkeyを作成すると思います。

$ php artisan key:generate

こちらのkeyを旧プロジェクトにdefineやconstなどで定義しておきます。

define('LARAVEL_APP_KEY', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz');

また、 laravelの config/app.php に設定されている cipher の値を同じく定数化しておきます。 laravel5.8のデフォルトだと AES-256-CBC になっていると思います。

define('LARAVEL_CIPHER', 'AES-256-CBC');

この2つは 暗号化されたsessionIdを作成するために必要です。

6. php.serialize_handlerをphp_serializeに変更

sessionがシリアライズされる方法をphpserialize() で実行された状態と同じにします。

ini_set('session.serialize_handler', 'php_serialize');

7. SessionHandlerを作成

<?php

namespace CompanyName\Session;

use CompanyName\Util\Util;

class AppSessionHandler implements \SessionHandlerInterface
{
    /**
     * @var string
     */
    protected $decryptSessionId;

    /**
     * @var \PDO
     */
    protected $pdo;

    /**
     * AppSessionHandler constructor.
     * @param string $decryptSessionId
     * @param \PDO $pdo
     */
    public function __construct(string $decryptSessionId, \PDO $pdo)
    {
        $this->decryptSessionId = $decryptSessionId;
        $this->pdo              = $pdo;
    }

    /**
     * @param string $savePath
     * @param string $name
     * @return bool
     */
    public function open($savePath, $name)
    {
        return true;
    }

    /**
     * @param string $sessionId
     * @return string
     */
    public function read($sessionId)
    {
        $stmt = $this->pdo->prepare("SELECT payload FROM sessions WHERE id = :id");
        $stmt->bindValue(":id", $this->decryptSessionId);
        $stmt->execute();
        $results = $stmt->fetchColumn();
        $stmt->closeCursor();
        return is_null($results) ? '' : base64_decode($results);
    }

    /**
     * @param string $sessionId
     * @param string $sessionData
     * @return bool
     */
    public function write($sessionId, $sessionData)
    {
        $sql =<<<SQL
INSERT INTO sessions (id, ip_address, user_agent, payload, last_activity)
VALUES(:id, :ip_address, :user_agent, :payload, :last_activity)
ON DUPLICATE KEY UPDATE last_activity = :last_activity, payload = :payload
SQL;

        $payload   = $this->getSavePayload($sessionId, $sessionData);
        $stmt = $this->pdo->prepare($sql);
        $stmt->bindValue(":id", $this->decryptSessionId);
        $stmt->bindValue(":ip_address", Util::ip());
        $stmt->bindValue(":user_agent", Util::userAgent());
        $stmt->bindValue(":payload", base64_encode(serialize($payload)));
        $stmt->bindValue(":last_activity", Util::currentTime());
        $stmt->execute();
        $results = $stmt->rowCount();
        $stmt->closeCursor();
        return ($results) ? true : false;
    }

    /**
     * @param string $sessionId
     * @return bool
     */
    public function destroy($sessionId)
    {
        $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE id = :id");
        $stmt->bindValue(":id", $this->decryptSessionId);
        $stmt->execute();
        $stmt->closeCursor();
        return true;
    }

    /**
     * @return bool
     */
    public function close()
    {
        return true;
    }

    /**
     * @param int $maxlifetime
     * @return bool
     */
    public function gc($maxlifetime)
    {
        $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE last_activity < :lastActivity");
        $stmt->bindValue(":lastActivity", $maxlifetime);
        $stmt->execute();
        $stmt->closeCursor();
        return true;
    }

    /**
     * @param string $sessionId
     * @param string $sessionData
     * @return array
     */
    protected function getSavePayload(string $sessionId, string $sessionData):array
    {
        $registeredPayload = $this->read($sessionId);
        if (strlen($registeredPayload) >= 1) {
            $registeredPayload = unserialize($registeredPayload);
        } else {
            $registeredPayload = $this->initPayload();
        }

        if (strlen($sessionData) >= 1) {
            $payload = array_merge($registeredPayload, unserialize($sessionData));
        } else {
            $payload = $registeredPayload;
        }
        $payload['_previous'] = ['url' => Util::referer()];

        return $payload;
    }

    /**
     * payloadの初期化
     * @return array
     */
    protected function initPayload():array
    {
        return $payload = [
            '_token'    => \Illuminate\Support\Str::random(40),
            '_flash'    => ['old' => [], 'new' => []]
        ];
    }
}

対応した部分はここまでになります。

これらを設定したのが以下のコードです。

<?php
define("LARAVEL_APP_KEY", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
define("SESSION_NAME", "xxxxxxxxxxxxxxxxxxxxx");
define("SESSION_TIME", time() + (120 * 60));
define("SESSION_PATH", "/");
define("SESSION_DOMAIN", null);
define("SESSION_SECURE", false);
define("SESSION_HTTP_ONLY", true);
define("LARAVEL_CIPHER", "AES-256-CBC");
ini_set('session.serialize_handler', 'php_serialize');

$encryptValue = base64_decode(substr(LARAVEL_APP_KEY, 7));
$encrypter    = new \Illuminate\Encryption\Encrypter($encryptValue, LARAVEL_CIPHER);

if (!isset($_COOKIE[SESSION_NAME])) {
    $sessionId        = \Illuminate\Support\Str::random(40);
    $encryptSessionId = $encrypter->encrypt($sessionId, false);
    setcookie(SESSION_NAME,  $encryptSessionId, SESSION_TIME,SESSION_PATH, SESSION_DOMAIN, SESSION_SECURE, LARAVEL_CIPHER);
} else {
    $decryptSessionId = $encrypter->decrypt($_COOKIE[SESSION_NAME], false);
    $pdo              = \CompanyName\Util\Db::getInstance(DBNAME, DBHOST, DBUSER, DBPASSWORD);
    $sessionHandler   = new \CompanyName\Session\AppSessionHandler($decryptSessionId, $pdo);
    session_name(SESSION_NAME);
    session_set_save_handler($sessionHandler, true);
}

これで、旧システムでSessionを使うような処理(ログインなど)を行っている状態で、laravelページに遷移後、 session()->all() などを行うと旧プロジェクトでのSession情報が取れ、その逆も可能になりました。

なんか抜けもありそうだから、もうちょい調査しますが、とりあえずここまで。