Codeignitor はじめました。のその後。

こんにちは。システム部の鈴木です。
先日、新年会で肥ったといじられました。暖かくなってから本気出します。

以前書いてからだいぶ間が空いてしまったCodeIgnitorを使って早速社内向けのサービスを作ってみました。
今回はそのときに気がついた点とか書いていこうと思います。

 

その前に、実際作ったもの

140131

タイムカードの代わりにブラウザでぽちるシステムです。
一週間の出社時間、退勤時間、あとお昼休憩の開始と終了の時刻を管理しています。
だれがどれだけ有給消化したか、なんかがわかります。
見える化ってやつですね。最近聞かない気もしますが。

 

CSSフレームワークはTwitter Bootstrap を使用し、テーマは BootSwatchで配布されているFlatlyをそのまま使用してます。
[browser-shot width=”400″ url=”http://bootswatch.com/”]

そして主役のCodeIgnitor

実際に使ってみた感想としては、シンプルなので学習コストを抑えて手軽に導入できる事はとてもよかったです。
独自にフレームワークを拡張する事にも柔軟に対応できるので、割と広く使えそうな印象はありました。
日本語ドキュメントが割と充実していた事もポイントですね。

 

…ですが。

デフォルトのままでは正直、いただけない設計・実装もありました。
特になんだこれは!?と思ったのが

セッションデータを全てCookieに保存してしまう謎の仕様

セッションデータ自体はファイルベース、DBでの管理を application/config/config.php の $config[‘sess_use_database’] で選択できるようになっていますが
その中身全てをなんとCookieにそのまま書き込んでしまいます。

ログイン機能を作るときなどはセッションIDだけをCookieに保存して、それによりセッションを引き継ぐような作りが普通だと思いますが
何故かセッションに保存されたものはそのままCookieに保存されるので、
ローカルのCookieの中身をみればセッションに保存した情報が丸見えという非常によろしくない実装になってしまいます。

そこでCodeIgnitor ではCookieに保存する際に暗号化して保存する仕組みが備わっています。
ユーザガイドにもありますが設定方法は以下の通りです。

  1. $config[‘encryption_key’] で saltを設定(これは暗号化にしない場合でも常に必要) 
  2. $config[‘sess_encrypt_cookie’] = true にする

これなら(解読さえされなければ)中身は見えなくなります。


…ですが。。

ユーザガイドの同ページにはこんな記述があります。

Note: クッキーは 4KB のデータだけを保持できますので、許容量を超えないよう注意してください。 ここの暗号化処理をすると、元の文字列より長い文字列になりますので、どの程度のデータを保存したのかを把握するよう注意してください。

セッションデータを更新する度にサイズを確認しないといけない、なんてそんな非効率な事だれがやりたいでしょうか。。
※そもそも暗号化したところで可逆暗号なら解読もできるはずで意味ないし。。

 

というわけで、弊社ではCodeIgnitorのセッションクラスは使うの禁止しました。

そして、セッションクラス作りましたので晒してみる事にしました。
ライセンスはGPLとします。

[php]<?php if ( ! defined(‘BASEPATH’)) exit(‘No direct script access allowed’);

/**
* 独自のSessionハンドラクラス(php 5.4 >=)
*
* CodeIgnitor の設定を利用し、独自に実装したセッションハンドラクラスです
* セッションデータの操作に関するメソッドはオリジナルのクラスと異なります
*
* DBでセッションデータを管理する場合は、オリジナルと同様に以下のスキーマで
* セッションテーブルを作成してください
*
* CREATE TABLE IF NOT EXISTS `ci_sessions` (
* session_id varchar(40) DEFAULT ‘0’ NOT NULL,
* ip_address varchar(16) DEFAULT ‘0’ NOT NULL,
* user_agent varchar(255) NOT NULL,
* last_activity int(10) unsigned DEFAULT 0 NOT NULL,
* user_data text NOT NULL,
* PRIMARY KEY (session_id),
* KEY `last_activity_idx` (`last_activity`)
* );
*
* via http://codeigniter.jp/user_guide_ja/libraries/sessions.html
*
* CodeIgnitor の設定ファイル config.php で sess_use_database の設定に基づき
* 使用するセッションハンドラを切り替えます。
*
* @author JunSuzuki<jun_suzuki@medical-desgin.co.jp>
*/
class Ex_Session
{
/**
* セッションのネームスペース
* @access private
* @param string
*/
private $_namespace = ‘default’;

/**
* コンストラクタ
*
* @access public
* @return void
*/
public function __construct()
{
ini_set(‘session.use_cookies’, 0);
ini_set(‘session.use_only_cookies’, 0);

$handler = new Ex_SessionHandler_File();
if ($handler->isHandlerType() !== true) {
$handler = new Ex_SessionHandler_Db();
}

// セッションハンドラの設定
session_set_save_handler(
array($handler, ‘open’),
array($handler, ‘close’),
array($handler, ‘read’),
array($handler, ‘write’),
array($handler, ‘destroy’),
array($handler, ‘gc’)
);

$handler->start();
}

/**
* セッションから値を取得する
*
* @access public
* @param string $key
* @param string $namespace
* @return mixed or false
*/
public function get($key, $namespace = ”)
{
if (empty($key)) return false;

$space = ! empty($namespace) ? $namespace : $this->_namespace;
if (empty($space) || ! isset($_SESSION[$space])) return false;

if (isset($_SESSION[$space][$key])) return $_SESSION[$space][$key];

return false;
}

/**
* セッションに値を保存する
*
* @access public
* @param string $key
* @param mixed $value
* @param string $namespace
* @return mixed or false
*/
public function save($key, $value, $namespace = ”)
{
if (empty($key)) return false;

$space = ! empty($namespace) ? $namespace : $this->_namespace;
if (empty($space)) return false;

if ($this->setNameSpace($space)) {
$_SESSION[$space][$key] = $value;
return true;
}
return false;
}

/**
* セッションから値を破棄する
*
* @access public
* @param string $key
* @param string $namespace
* @return mixed or false
*/
public function delete($key, $namespace = ”)
{
if (empty($key)) return false;

$space = ! empty($namespace) ? $namespace : $this->_namespace;
if (empty($space) || ! isset($_SESSION[$space])) return false;

if (isset($_SESSION[$space][$key])) {
unset($_SESSION[$space][$key]);
return true;
}

return false;
}

/**
* セッションに保存された名前空間の一覧を取得する
*
* @access public
* @param string $key
* @param string $namespace
* @return array or false
*/
public function getNameSpaces()
{
if (empty($name) || count($_SESSION) <= 0) return false;

$names = array();
foreach ($_SESSION as $name) {
$names[] = $name;
}

return $names;
}

/**
* セッションに名前空間を保存する
*
* @access public
* @param string $key
* @param string $namespace
* @return bool
*/
public function setNameSpace($name)
{
if (empty($name)) return false;

if (! isset($_SESSION[$name])) $_SESSION[$name] = array();
return true;
}

/**
* セッションから指定の名前空間を破棄する
*
* @access public
* @param string $name
* @return bool
*/
public function unsetNameSpace($name)
{
if (empty($name) || ! isset($_SESSION[$name])) return false;
unset($_SESSION[$name]);
return true;
}

/**
* セッションデータを破棄する
*
* @access public
* @param string $name
* @return bool
*/
public function clear()
{
setcookie(
$this->cookie_prefix . $this->sess_cookie_name,
session_id(),
time() – 42000,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure,
false
);

session_destroy();
return true;
}
}

/**
* DBを使用するセッションハンドラ
*/
class Ex_SessionHandler_Db extends Ex_SessionHandler
{
/**
* セッションのオープン
*
* @access public
* @param void
* @return void
*/
public function open($savePath, $sessionName)
{
return $this->_db->table_exists($this->sess_table_name);
}

/**
* セッションをクローズする
*
* @access public
* @param void
* @return void
*/
public function close()
{
return true;
}

/**
* セッションのデータを読み込む
*
* @access public
* @param string $id
* @return void
*/
public function read($id)
{
$data = $this->_getSessionById($id);
if (isset($data[‘user_data’])) {
return unserialize(stripslashes($data[‘user_data’]));
}

return ”;
}

/**
* セッションにデータを書き込む
*
* @access public
* @param string $id
* @param array $data
* @return void
*/
public function write($id, $data)
{

$insertData = array(
‘session_id’ => $id,
‘ip_address’ => $_SERVER[‘REMOTE_ADDR’],
‘user_agent’ => $_SERVER[‘HTTP_USER_AGENT’],
‘last_activity’ => mktime(date(‘H’), date(‘i’), date(‘s’), date(‘m’), date(‘d’), date(‘Y’)),
‘user_data’ => stripslashes(serialize($data)),
);

$this->_db->replace($this->sess_table_name, $insertData);

return true;
}

/**
* セッションを破棄する
*
* @access public
* @param string $id
* @return void
*/
public function destroy($id)
{
return $this->_db->delete($this->sess_table_name, array(‘session_id’ => $id));
}

/**
* ガベージコレクト
*
* @access public
* @param void
* @return void
*/
public function gc()
{
$this->_db->delete($this->sess_table_name, array(‘last_activity < ‘ => time() – $this->sess_expiration));
return true;
}

public function isHandlerType()
{
return $this->sess_use_database;
}

/**
* セッション情報をIDから取得する
*
* @access private
* @param string $sid
* @return array or false
*/
protected function _getSessionById($sid)
{
$this->_db->where(‘session_id’, $sid);
$query = $this->_db->get($this->sess_table_name);
$result = $query->row_array();

if (empty($result)) return false;

if ($this->sess_match_ip == true && $result[‘ip_address’] != $_SERVER[‘REMOTE_ADDR’]) {
return false;
}

if ($this->sess_match_useragent == true && $result[‘user_agent’] != $_SERVER[‘HTTP_USER_AGENT’]) {
return false;
}

return $result;
}
}

/**
* ファイルベースのセッションハンドラ
*/
class Ex_SessionHandler_File extends Ex_SessionHandler
{
/**
* セッションのオープン
*
* @access public
* @return void
*/
public function open($savePath, $sessionName)
{
$this->savePath = $savePath;
if (!is_dir($this->savePath)) {
mkdir($this->savePath, 0777);
}

return true;
}

/**
* セッションをクローズする
*
* @access public
* @return void
*/
public function close()
{
return true;
}

/**
* セッションのデータを読み込む
*
* @access public
* @return void
*/
public function read($id)
{
return (string)@file_get_contents("$this->savePath/sess_$id");
}

/**
* セッションにデータを書き込む
*
* @access public
* @return void
*/
public function write($id, $data)
{
return file_put_contents("$this->savePath/sess_$id", $data) === false ? false : true;
}

/**
* セッションを破棄する
*
* @access public
* @return void
*/
public function destroy($id)
{
$file = "$this->savePath/sess_$id";
if (file_exists($file)) {
unlink($file);
}

return true;
}

/**
* ガベージコレクト
*
* @access public
* @return void
*/
public function gc()
{
foreach (glob("$this->savePath/sess_*") as $file) {
if (filemtime($file) + $maxlifetime < time() && file_exists($file)) {
unlink($file);
}
}

return true;
}

public function isHandlerType()
{
if ($this->sess_use_database === true) {
return false;
}
return true;
}

/**
* セッション情報をIDから取得する
*
* @access private
* @param string $sid
* @return array or false
*/
protected function _getSessionById($sid)
{
$file = "$this->savePath/sess_$id";
if (file_exists($file)) {
return true;
}

return false;
}
}

/**
* セッションハンドラの基底クラス
* CoreIgnitor の設定を利用し、セッションの機能を提供する
*
* @author JunSuzuki<jun_suzuki@medical-design.co.jp>
* @version 0.1
*/
class Ex_SessionHandler
{
protected $sess_encrypt_cookie = false;
protected $sess_use_database = false;
protected $sess_table_name = ”;
protected $sess_expiration = 7200;
protected $sess_expire_on_close = false;
protected $sess_match_ip = false;
protected $sess_match_useragent = true;
protected $sess_cookie_name = ‘ci_session’;
protected $cookie_prefix = ”;
protected $cookie_path = ”;
protected $cookie_domain = ”;
protected $cookie_secure = false;
protected $sess_time_to_update = 300;
protected $encryption_key = ”;
protected $flashdata_key = ‘flash’;
protected $time_reference = ‘time’;
protected $gc_probability = 5;
protected $userdata = array();
protected $CI;
protected $now;

protected $_db;

/**
* コンストラクタ
*
* @access public
* @return void
*/
public function __construct()
{
$this->CI =& get_instance();

// configファイルによりメンバ変数の値を定義
$keys = array(
‘sess_encrypt_cookie’,
‘sess_use_database’,
‘sess_table_name’,
‘sess_expiration’,
‘sess_expire_on_close’,
‘sess_match_ip’,
‘sess_match_useragent’,
‘sess_cookie_name’,
‘cookie_path’,
‘cookie_domain’,
‘cookie_secure’,
‘sess_time_to_update’,
‘time_reference’,
‘cookie_prefix’,
‘encryption_key’
);

foreach ($keys as $key) {
$this->$key = (isset($params[$key])) ? $params[$key] : $this->CI->config->item($key);
}

if ($this->sess_use_database) {
$this->_db = $this->CI->load->database(‘default’, true);
}

register_shutdown_function(‘session_write_close’);
}

/**
* セッションを開始する
*
* @access public
* @param int $expire
* @return void
*/
public function start($expire = 0)
{
// セッション名が設定されていればそれを指定
if (! empty($this->sess_cookie_name)) {
session_name($this->cookie_prefix . $this->sess_cookie_name);
}

// セッションIDがあれば指定
$sid = $this->getSessionId();
if (! empty($sid)) session_id($sid);

// 有効期限の指定が無ければ設定ファイルに基づいて指定する
$expire = ! empty($expire) && is_numeric($expire) ? $expire : $this->sess_expiration;
$domain = ! empty($this->cookie_domain) ? $this->cookie_domain : $_SERVER[‘HTTP_HOST’];

ini_set(‘session.gc_maxlifetime’, $expire);

session_start();
session_regenerate_id(true);

setcookie(
$this->cookie_prefix . $this->sess_cookie_name,
session_id(),
time() + $expire,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure,
false
);

log_message(‘debug’, "EX_Session routines successfully run");
}

/**
* セッションIDを取得する
*
* @access public
* @param void
* @return string or false
*/
public function getSessionId()
{
$sid = ”;

// Cookieでセッションを引き継いでいる場合
if (isset($_COOKIE[$this->sess_cookie_name]) && ! empty($_COOKIE[$this->sess_cookie_name])) {
$sid = $_COOKIE[$this->sess_cookie_name];

// POST で引き継いでいる場合
} elseif (isset($_POST[$this->sess_cookie_name]) && ! empty($_POST[$this->sess_cookie_name])) {
$sid = $_POST[$this->sess_cookie_name];

// GET で引き継いでいる場合(主にフィーチャーフォン)
} elseif (isset($_GET[$this->sess_cookie_name]) && ! empty($_GET[$this->sess_cookie_name])) {
$sid = $_GET[$this->sess_cookie_name];
}

if (! empty($sid) && $this->_getSessionById($sid)) {
return $sid;
}

return false;
}
}
[/php]

 

セッションデータの管理はファイルベースでもDBでも大丈夫ですが、
DBでの仕様のみを想定しているためファイルベースではネームスペースなど無視しています。(※今後改善予定)
あとはCIの設定ファイルに基づくように作ってあります。
 

よくある自前のセッションハンドラの実装とほぼ同じなので特別な事は特にしていませんが
Codeignitorデフォルトのは使いたくない場合は参考にしてみてください。