PHPのセッション保存周りについて

PHPのセッションとセッションハンドラ

PHP は session_start(); をコールしたあとで $_SESSION にアクセスするだけでお手軽にセッションを利用することができます(ここではセキュリティの話は置いておく)が、よく知られた通り、デフォルトではこのとき $_SESSION 連想配列の内容がシリアライズされた上でローカルのファイルシステム上に書き込まれるようになっています。

ただ、ファイルに書き込みする場合、たとえば上流にロードバランサを置いて2つ以上のアプリケーションサーバに負荷を分散するような場合に2台のサーバー間でセッションデータを共有できなくなってしまいますね。ロードバランサ自体がL7まで解析して、セッションidが一緒なら必ず同じアプリケーションサーバーにリクエスト流すみたいなことしてもいいんだけど、こういう場合はローカルのファイルシステムじゃなくてどっか別のストレージにセッションデータを保存するのが筋の良い対応だろうと思います。

そのあたり、PHP は実は結構偉くて、SessionHandler というクラスを継承してそれをハンドラとして登録することで、「シリアライズされたセッションデータをどうやって保存したり読み込んだりするか」という動きをカスタマイズできる仕組みが用意されています。このあたりの詳しい話はマニュアル( http://php.net/manual/ja/class.sessionhandler.php ) を読んでほしい。

これを利用することによって、PHPのセッションデータをファイルシステム上ではなく Redis とか RDB とかに保存することが可能になるというスンポーですね。

ちなみに、 PHP: 導入 - Manualにあるように、memcached 拡張ライブラリなんかは公式でセッションハンドラが用意されているので、こういうのはがんがんつかっちゃうのが良さそうですね。redisに関してもこういうのがあるようです。(Thanks to @uzulla)

一方、$_SESSION を直接触らないと割り切ってしまって pure PHP の世界でセッションを再実装してしまうという方式もWAFなんかではよく取られているようですね。

$_SESSION のシリアライズについて

さて、上述の通り、PHPは $_SESSION の値の保存先に関してはかなり柔軟に対応できるような仕組みが言語内に組み込まれているわけですが、ここで問題にしたいのは「じゃあシリアライズの形式ってどうなってるの」という話です。

PHP は標準で serialize 関数と unserialize 関数を持っていて、これらは名前の通り PHP プログラム内での値をシリアライズ、デリシリアライズする機能を持っています。例としてこういう感じ。

$ php -r 'echo serialize(1);'
i:1;

$ php -r 'echo serialize([1,2,3]);' 
a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}

$ php -r 'echo serialize(["a" => 1,"b" => 2,"c" => 3]);'
a:3:{s:1:"a";i:1;s:1:"b";i:2;s:1:"c";i:3;}

こういうものが既に存在するので、PHPのセッションデータもこれにならっているであろう、という予想ができるわけですが、実はPHPのセッションのシリアライズはこれとは微妙に異なる動きをするのです。マニュアルを引いてみましょう。

PHP: session_encode - Manual

"シリアライズの方法は serialize() とは違うことに注意しましょう。"!

"シリアライズの方法は serialize() とは違うことに注意しましょう。"!!!!!

"シリアライズの方法は serialize() とは違うことに注意しましょう。"!!!!!!!!!

な、なんだってーーー!!!!!(AA略)じゃあどうなっているのか。実際に試してみましょう。

$ php -r 'session_start(); $_SESSION = ["a" => 1, "b" => 2, "c" => 3]; echo session_encode();' 
a|i:1;b|i:2;c|i:3;

oh... たしかに違う……。もうちょっと深追いしましょう。マニュアルには "PHP が内部的に使うシリアライズ方法は session.serialize_handler で設定できます。" とありますね。じゃあ session.serialize_handler を引きましょう。

PHP: 実行時設定 - Manual

session.serialize_handler string

session.serialize_handler は、シリアル化または シリアル化データを復元するために使用されるハンドラの名前を定義します。 PHP シリアライズフォーマット (php_serialize)、 PHP 内部フォーマット (php あるいは php_binary)、 そして WDDX (wddx) に対応しています。WDDX は、PHP がWDDX サポート を有効にしてコンパイルされている場合のみ使用可能です。 php_serialize は PHP 5.5.4 以降で使用可能です。 php_serialize はプレーンな serialize/unserialize 関数を内部的に使っており、phpphp_binary のような制約はありません。これらのシリアライズハンドラでは、$_SESSION の中で数値のインデックスや特殊文字 (| や !) を含む文字列のインデックスを使えませんでした。 php_serialize を使えば、 スクリプトの終了時に数値インデックスや特殊文字インデックスのエラーが出ないようにできます。 デフォルトは php です。

ふむ、とにかくデフォルトは php というハンドラのようです。で、この挙動が気に食わない場合は php_serialize とか php_wddx というものが使えるようですね。

それはともかく、デフォルトの php というハンドラについて調べてみましょう。これはちょっとマニュアル引いてもわかんなかったので、実際のソースコードを読みましょう。この関数ですね。

ふむ、たしかに上でみた動き通りのコードになっていますね。

いやでもこれちょっと待てよ、この行に注目しましょう。

!!!!php_var_serialize!!!!

おい内部的に php の serialize 関数呼んでるやんけ!!!!!!!という事実が見えてきました。じゃあ最初からデフォルトを php_serialize にすれば良かったのではないかという気持ちがむくむくとわき起こるわけですが、このあたりの歴史的経緯を知ってるひとはわたしに是非教えてください。

とりあうずのまとめ

  • PHP では $_SESSION は
    1. シリアライズされて
    2. 保存されるが、
  • 保存のやり方は SessionHandler を利用してカスタマイズできる
  • シリアライズの方式は session.serialize_handler を設定することで選ぶことが可能である
    • しかしなぜわざわざデフォルトでは serialize と別のフォーマットで保存しているかは謎である、だれか教えてください

言い残したこと

これらの知見は php_session gemRubyからPHPのセッションを触る君です)をファイル以外のストレージに対応しようとしている間に得られた知見です。php_session gem は無事ファイル以外のストレージに対応する仕組みを組み込むことができましたが、session.serialize_handler のほうを真面目に対応するとなると結構だるいぞこれ(つまりそれってパーサーをそれだけ書かないといけないということだからね!!!)、というところで、もしもデフォルトのセッションシリアライザ以外を PHP で使ってて、しかもそれを Ruby で読みたいというニッチすぎて世界にひとつもないんじゃないかというニーズをお持ちの方はぜひpull requestくださいね。ニッチすぎるのでわたしが対応するつもりはありません。