まいける's Tech Blog

LAMP関係のメモなどを不定期に掲載します

Laravel のセッションを非 Laravel のプロジェクトと共有する

基本的な手順は kojirockさんのこちらの記事 を参考にさせていただきました。ありがとうございます。

1. 必要なライブラリをインストール

$ composer require illuminate/encryption

2. Laravel用sessionテーブル作成

3. Laravel側session設定

Laravel側の設定なので、このあたりを参照してください

4. Laravel側session設定を共有したいプロジェクトで定数として設定

例えばこんな感じ

<?php
define("LARAVEL_APP_KEY", "base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
define("SESSION_NAME", "laravel_session");
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");

define("DBNAME", "dbname");
define("DBHOST", "127.0.0.1");
define("DBUSER", "hoge");
define("DBPASSWORD", "fuga");

5. SessionHandlerを作成

kojirock さんの記事を参考にしつつ、session_regenerate_id に対応できるように create_sid メソッドを追加しています

<?php
namespace MyApp\Session;

use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;

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

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

    /**
     * AppSessionHandler constructor.
     * @param string $decryptSessionId
     * @param \PDO $pdo
     */
    public function __construct($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;

        if ($this->isEncryptSessionId($sessionId)) {
            $sessionId = $this->decryptSessionId($sessionId);
        }

        $payload = $this->getSavePayload($sessionId, $sessionData);
        $stmt = $this->pdo->prepare($sql);
        $stmt->bindValue(":id", $sessionId);
        $stmt->bindValue(":ip_address", $_SERVER['REMOTE_ADDR']);
        $stmt->bindValue(":user_agent", $_SERVER['HTTP_USER_AGENT']);
        $stmt->bindValue(":payload", base64_encode(serialize($payload)));
        $stmt->bindValue(":last_activity", time());
        $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;
    }

    /**
     * 新しい sessionIDの作成
     * @return string
     */
    public function create_sid()
    {
        $sessionId = Str::random(40);
        return $sessionId;
    }

    /**
     * @param string $sessionId
     * @param string $sessionData
     * @return array
     */
    protected function getSavePayload($sessionId, $sessionData)
    {
        // DB から保存されているデータを取得
        $registeredPayload = $this->read($sessionId);
        if (strlen($registeredPayload) >= 1) {
            $registeredPayload = unserialize($registeredPayload);
        } else {
            // 保存されているデータがなければ初期化
            $registeredPayload = $this->initPayload();
        }

        if (strlen($sessionData) >= 1) {
            // セッションとして登録したいデータがあれば上書き
            $payload = unserialize($sessionData);
        } else {
            // セッションとして登録したいデータがなければデータを保持
            $payload = $registeredPayload;
        }
        $payload['_previous'] = ['url' => $_SERVER['HTTP_REFERER']];

        return $payload;
    }

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

    /**
     * 暗号化された sessionID かどうかの判別
     * @param $sessionId
     * @return bool
     */
    protected function isEncryptSessionId($sessionId)
    {
        if (is_string($sessionId) && ctype_alnum($sessionId) && strlen($sessionId) === 40) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * 暗号化された sessionID の復号
     * @param $sessionId
     * @return string
     */
    protected function decryptSessionId($sessionId)
    {
        $encryptValue = base64_decode(substr(LARAVEL_APP_KEY, 7));
        $encrypter = new Encrypter($encryptValue, LARAVEL_CIPHER);

        $decryptSessionId = $encrypter->decrypt($sessionId);
        return $decryptSessionId;
    }

}

6. カスタムセッションハンドラの設定

4. で設定した定数を /var/www/config.php に、5. で設定したSessionHandlerを /var/www/class_session_handler.php に置いていたとして

<?php
require_once '/var/www/config.php';
require_once '/var/www/class_session_handler.php';

use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;
use MyApp\Session;

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

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

if (!isset($_COOKIE[SESSION_NAME])) {
    $sessionId = Str::random(40);
    $encryptSessionId = $encrypter->encrypt($sessionId);
    setcookie(SESSION_NAME, $encryptSessionId, SESSION_TIME, SESSION_PATH, SESSION_DOMAIN, SESSION_SECURE,
        SESSION_HTTP_ONLY);
} else {
    if (is_string($_COOKIE[SESSION_NAME]) && ctype_alnum($_COOKIE[SESSION_NAME]) && strlen($_COOKIE[SESSION_NAME]) === 40) {
        // regenerate された session_id のときは session_id を暗号化して再格納
        $encryptSessionId = $encrypter->encrypt($_COOKIE[SESSION_NAME]);
        setcookie(SESSION_NAME, $encryptSessionId, SESSION_TIME, SESSION_PATH, SESSION_DOMAIN, SESSION_SECURE,
            SESSION_HTTP_ONLY);
        $decryptSessionId = $_COOKIE[SESSION_NAME];
    } else {
        $decryptSessionId = $encrypter->decrypt($_COOKIE[SESSION_NAME]);
    }
    $dsn = 'mysql:host=' . DBHOST . ';dbname=' . DBNAME;

    $pdo = new PDO($dsn, DBUSER, DBPASSWORD);
    $sessionHandler = new Session\AppSessionHandler($decryptSessionId, $pdo);
    session_name(SESSION_NAME);
    session_set_save_handler($sessionHandler, true);
}

このあとに session_start() を呼び出せば、Laravel と非 Laravel の双方で読み書きできるセッションが作れると思います