いえらぶ > ブログ > 記事詳細

  • faebook
  • ツイッター
  • グーグル+
  • bookmark
  • LINEで送る
  • pocket

PHPのセッション情報をデータベース(MySQL)に保存するぜ!

はじめまして鰆目です。「さわらめ」と読みます。「さーらめ」でも大丈夫です。
息子を可愛がり過ぎるあまり、妻から将来モンスターペアレントになってしまうのではないかと心配されています。

さて、セッションと聞いて何を思い浮かべますか?即興で音楽を奏でる姿を想像したJAZZYなあなたには今回の話は関係ないかもしれません。
ここで言うセッションとはWEBサーバーにアクセスしてきたユーザーのデータを個別に格納する仕組みのことです。そう、システム関係のお話です。

ギター

PHPのセッション情報

PHPのセッション情報は通常はphp.iniのsession.save_pathに設定されているパスにsess_********と言ったファイル名で保存されています。********はブラウザに保存されているcookieのPHPSESSID(session.nameで変更可)の値です。cookieにユニークなセッションのIDを保存させてサーバー上のセッション情報が重複しないようになっているんですね。

今回はこのセッション情報をデータベースに保存しましょうって話です。

セッション情報をデータベースに保存すると何が嬉しいのか

ロードバランサーなんかで複数台のWEBサーバーへ負荷分散をしている構成でセッションをそのまま各サーバーのローカルに保存している場合、接続するサーバーが変わるたびにセッション情報が見つからなくなっちゃいます。ログインするサービスなんかだと勝手にログアウトしちゃうってことになっちゃいますね。複数のWEBサーバー共通のデータベースサーバーに保存しておくと共通のセッション情報を参照できるようになるので、接続先WEBサーバーが変わってもセッション情報が引き継げる様になります。

また、通常の場合はWEBサーバーよりもデータベースサーバーのほうがスペックが良い場合が多く、その恩恵を受けてパフォーマンスを改善することも期待できるのです。

セッション保存用データベース作成

まず、何はともあれセッション保存用データベースを作成します。使用するDBMSはMySQLです。

mysql> create database test;
Query OK, 1 row affected (0.00 sec)

mysql> use test
Database changed
mysql> CREATE TABLE `session` (
    ->   `id` varchar(100) NOT NULL,
    ->   `data` longtext,
    ->   `created` datetime DEFAULT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.01 sec)

testスキーマを作成してその中にsessionテーブルを作成しました。今回はこのsessionテーブルの中にセッションの情報を保存します。テーブルの各フィールドは以下のように使用します。

id・・・セッションID。cookieのPHPSESSIDの値。
data・・・セッションデータ。ここにセッションのデータが保存されます。
created・・・セッション作成日時。データが更新されるたびにここの日付も更新する。

セッションの更新日時を持っておくのはsession.gc_maxlifetimeを過ぎたセッションをガーッと消すためです。いつまでもデータが残っていたらレコード数が増える一方ですからね。

セッションクラスを作成する

今度は作成したテーブルにセッション情報を保存するPHPのクラスを作成します。

<?php
class MysqlSessionHandler
{
    /**
     * MySQLサーバーへの接続先
     * @var string
     */
    private $_host = 'localhost';

    /**
     * MySQLサーバーの接続ユーザー
     * @var string
     */
    private $_user = 'root';
    /**
     * MySQLサーバーの接続パスワード
     * @var string
     */
    private $_pass = '';
    /**
     * MySQLサーバーの接続データベース
     * @var string
     */
    private $_db = 'test';

    /**
     * MySQL接続情報を持つ
     * @var mysqli
     */
    private $_link = null;

    /**
     * セッションを開始する際に呼び出される。MySQLへの接続を開始する。
     * @param  string $savePath session.save_pathで設定されているパス
     * @param  string $saveName session.nameで設定されている名前(PHPSESSID)
     * @return bool
     */
    function open($savePath, $saveName)
    {
        $this->_link = mysqli_init();
        return mysqli_real_connect($this->_link, $this->_host, $this->_user, $this->_pass, $this->_db);
    }

    /**
     * セッションを閉じる際に呼び出される。MySLQへの接続を閉じる。
     * @return bool
     */
    function close()
    {
        if(!is_null($this->_link))
        {
            return mysqli_close($this->_link);
        }
        return true;
    }

    /**
     * セッションのデータを読み込む。対象のレコードを取り出してデータを返す
     * @param  string $id セッションID
     * @return string セッションのデータ
     */
    function read($id)
    {
        $id = mysqli_real_escape_string($this->_link, $id);
        $sql = "SELECT * FROM session WHERE id LIKE '${id}'" ;
        $result = mysqli_query($this->_link, $sql);
        $row = mysqli_fetch_assoc($result);
        if(!empty($row))
        {
            return $row['data'];
        }
        return null;
    }
    /**
     * セッションのデータを書き込む。レコードを追加・更新する
     * @param  string $id セッションID
     * @param  string $data セッションのデータ $_SESSIONをシリアライズしたもの
     * @return bool
     */
    function write($id, $data)
    {
        $id = mysqli_real_escape_string($this->_link, $id);
        $data = mysqli_real_escape_string($this->_link, $data);
        $date = date("Y-m-d H:i:s");
        $sql = "REPLACE INTO session VALUES ('${id}', '${data}', '${date}')" ;
        mysqli_query($this->_link, $sql);
        return true;
    }

    /**
     * セッションを破棄する。対象のレコードを削除します。
     * @param  string $id セッションID
     * @return bool
     */
    function destroy($id)
    {
        $id = mysqli_real_escape_string($this->_link, $id);
        $sql = "DELETE FROM session WHERE id LIKE '${id}'" ;
        mysqli_query($this->_link, $sql);
        return true;
    }

    /**
     * 古いセッションを削除する。古いレコードを削除します。
     * @param  string $maxlifetime セッションのライフタイム session.gc_maxlifetimeの値
     * @return bool
     */
    function gc($maxlifetime)
    {
        $maxlifetime = preg_replace('/[^0-9]/', '', $maxlifetime);
        $sql = "DELETE FROM session WHERE (TIMESTAMP(CURRENT_TIMESTAMP) - TIMESTAMP(created)) > ${maxlifetime}" ;
        mysqli_query($this->_link, $sql);
        return true;
    }
}
MysqlSessionHandler.php

セッションクラスに必要な関数は「open」「close」「read」「write」「destroy」「gc」の6つです。各関数の機能についてはソースコードのコメントを参照してください。
この作成した各関数をセッション情報操作関数として登録し実行することが出来ます。

それでは早速実行してみましょう。

セッションクラスを設定して実行

<?php
// データベースセッションクラスのインスタンス作成
require_once 'MysqlSessionHandler.php';
$handler = new MysqlSessionHandler();
// クラスを設定
session_set_save_handler(
    array($handler, 'open'),
    array($handler, 'close'),
    array($handler, 'read'),
    array($handler, 'write'),
    array($handler, 'destroy'),
    array($handler, 'gc')
);
// シャットダウンする際にセッション情報を書き込んでクローズ
register_shutdown_function('session_write_close');
// セッション開始
session_start();
// セッションに情報を設定
$_SESSION['abc'] = 'def';
$_SESSION['ghi'] = 'jkl';
index.php

上記の様にsession_set_save_handlerでセッションの情報操作に使用する関数を登録します。先ほど作成した6つの関数を指定します。最後にregister_shutdown_functionでsession_write_closeを設定するのがミソです。session_set_save_handlerにオブジェクトで指定した場合、オブジェクトが破棄された後にwrite/closeが実行されてしまうので、セッション情報が保存されないという現象を防ぐことが出来ます。

登録した関数は以下の順番で実行されます。

  1. session_start()で「open」が実行されDBへの接続を開始します。
  2. 「open」が実行された後「read」が実行されセッション情報が$_SESSIONに取得されます。
  3. スクリプトが終了すると「write」が実行され$_SESSIONの内容が書き込まれます。
  4. 「write」が実行された後「close」が実行されDBへの接続を切断します。

で、実際に登録されたデータは以下の様になりました。

mysql> select * from session;
+----------------------------+------------------------------+---------------------+
| id                         | data                         | created             |
+----------------------------+------------------------------+---------------------+
| o6sa322oqa82b3om50lvjvb757 | abc|s:3:"def";ghi|s:3:"jkl"; | 2015-03-20 14:06:47 |
+----------------------------+------------------------------+---------------------+
1 row in set (0.00 sec)

データが保存されています。2回目以降のアクセスで$_SESSIONにデータが入っているかも確認してみましょう。一回目のアクセスで保存したデータが確認できると思います。

作成はしたけど実行確認していない関数「destroy」はsession_destroy()された時に呼び出されます。session_destroy()を実行してデータが削除されるか確認してみてください。
「gc」は一定の確率で呼び出されます。この確率とはsession.gc_probability/session.gc_divisorで設定できます。デフォルトは1/100ですので100回スクリプトを実行すれば1回「gc」が実行されるということになります。

以上の手順でセッション情報をデータベースに保存することが出来ました。意外と簡単ですね。
PHP5.4以降ではセッションクラスにSessionHandlerInterfaceを使用することで、もっと簡単に実装することが出来ます。

<?php
// こんなかんじで
require_once 'MysqlSessionHandler.php';
$handler = new MysqlSessionHandler();
session_set_save_handler($handler, true);
session_start();

日々拡大し続けるサービスに対応する開発部

php5.5.1以降ではsession_set_save_handlerの7番目の引数にcreate_sidコールバックを指定してすることができるので、これを使って独自のセッションIDを発行するようにする事もできます。その場合はセッションハイジャック(cookieのセッションIDを書き換えてなりすましログインする攻撃)に気をつけないといけないのですが、掘り下げると長くなるので今回は省略します。

日々拡大し続けるサービスでは様々な対応・施策が必要になってきます。いえらぶCLOUDもまた然り。開発部からはいえらぶCLOUDで実装している施策を現場目線で詳細且つわかりやすく紹介していければと思っています。


ページトップ

戻る