會(huì)話和數(shù)據(jù)持久存儲(chǔ)

本文為《PHP經(jīng)典實(shí)例》閱讀筆記

前言

隨著web應(yīng)用日漸成熟,“有狀態(tài)性”成為一個(gè)常見(jiàn)的需求锈嫩,有狀態(tài)應(yīng)用已經(jīng)相當(dāng)普及酪夷,甚至被認(rèn)為是理所當(dāng)然的。有狀態(tài)應(yīng)用是指:訪問(wèn)者瀏覽網(wǎng)站時(shí)导梆,應(yīng)用能跟蹤記錄這個(gè)訪問(wèn)者的信息。雖然http被設(shè)計(jì)為無(wú)狀態(tài)協(xié)議,不過(guò)PHP提供了一組方便的會(huì)話管理函數(shù)问潭,使實(shí)現(xiàn)有狀態(tài)應(yīng)用更方便簡(jiǎn)單猿诸,后文將介紹開(kāi)發(fā)有狀態(tài)應(yīng)用時(shí)要謹(jǐn)記的一些優(yōu)秀實(shí)踐做法。

使用會(huì)話跟蹤

我們可以使用會(huì)話模塊來(lái)跟蹤用戶狡忙,如下面的一個(gè)例子:

session_start();
if (! isset($_SESSION['visit'])){
    $_SESSION['visit'] = 0;
}
$_SESSION['visit']++;
echo 'You have visited here '.$_SESSION['visit'].' times.';

會(huì)話模塊通過(guò)向用戶發(fā)送cookie來(lái)跟蹤用戶梳虽,cookie中包含隨機(jī)生成的session id,且cookie名為PHPSESSID灾茁。如果用戶不接受cookie窜觉,那么會(huì)在URL后加上?PHPSESSID=xxxx(id)北专,使之能傳遞到下一個(gè)頁(yè)面禀挫。明顯這樣的URL并不安全,比如一個(gè)用戶復(fù)制該URL并發(fā)送給其他人拓颓,那么無(wú)意間其他人便會(huì)假冒成該用戶訪問(wèn)網(wǎng)站语婴,因此默認(rèn)會(huì)禁止這種行為。要啟用URL中傳遞session id的功能驶睦,可以在開(kāi)始會(huì)話前使用ini_set('session.use_trans_sid',true)砰左。

防止會(huì)話劫持

為確保攻擊者不能訪問(wèn)另一用戶的會(huì)話,我們可以規(guī)定只允許通過(guò)cookie傳遞session id场航,并生成另外一個(gè)會(huì)話token通過(guò)URL傳遞缠导。只有包含一個(gè)合法session id和合法token才可以訪問(wèn)會(huì)話,如下面部分示例代碼:

ini_set('session.use_only_cookies', true);
//指定是否在客戶端僅僅使用 cookie 來(lái)存放會(huì)話 ID溉痢,啟用此設(shè)定可以防止有關(guān)通過(guò) URL 傳遞會(huì)話 ID 的攻擊僻造。
session_start();
$salt = 'YourSpecialValueHere';
$tokenstr = strval(date('W')).$salt;
$token = md5($tokenstr);

if (!isset($_REQUEST['token']) || $_REQUEST['token'] != $token){
    // 提示登錄
    echo "Please login";
    exit;
}

$_SESSION['token'] = $token;
output_add_rewrite_var('token', $token);

該例通過(guò)將當(dāng)前周數(shù)strval(date('W'))與變量$salt連接為一個(gè)字符串,創(chuàng)建一個(gè)自動(dòng)移位的token孩饼,保證token是不固定的且在一段時(shí)間內(nèi)是合法的髓削。
  然后檢查請(qǐng)求中的token【$_REQUEST具有$_POST和$_GET的功能,但相對(duì)來(lái)說(shuō)會(huì)比較慢】捣辆,如果未找到則提示重新登錄蔬螟,找到則將它添加到生成的鏈接【例如當(dāng)前頁(yè)面<a>標(biāo)簽的鏈接后作為get的參數(shù)】或者表單中【以input隱藏域形式】,以保證下一次請(qǐng)求能順利進(jìn)行汽畴。用output_add_rewrite_var()來(lái)實(shí)現(xiàn)上述功能。

防止會(huì)話固定攻擊

為確保應(yīng)用不會(huì)受到會(huì)話固定攻擊(攻擊者強(qiáng)制用戶使用一個(gè)預(yù)定義的會(huì)話ID)耸序,我們應(yīng)使用會(huì)話cookie但會(huì)話標(biāo)識(shí)符不會(huì)追加到url中忍些,同時(shí)頻繁生成新的會(huì)話ID。

ini_set('session.use_only_cookies', true);
session_start();
if (!isset($_SESSION['generated'])
    || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
}

該例首先設(shè)置會(huì)話行為坎怪,即只能用cookie存儲(chǔ)session id罢坝,確保PHP不會(huì)注意攻擊者放在URL中的session id。
  一旦會(huì)話開(kāi)始,設(shè)置一個(gè)值來(lái)記錄生成session id的最后時(shí)間嘁酿,定期生成一個(gè)新的session id隙券,該例所定時(shí)間為30秒,就能大大降低攻擊者得到合法session id的幾率闹司。
  這兩種方法結(jié)合娱仔,基本可以消除會(huì)話固定攻擊的風(fēng)險(xiǎn)。攻擊者很難得到一個(gè)合法的session id游桩,因?yàn)閕d會(huì)頻繁變化牲迫,另外由于session id只能在cookie中傳遞,因此基于url的攻擊是不可能的借卧。

在數(shù)據(jù)庫(kù)中存儲(chǔ)會(huì)話

我們可能希望在數(shù)據(jù)庫(kù)中存儲(chǔ)會(huì)話數(shù)據(jù)而不是在文件中盹憎,這時(shí)如果多個(gè)服務(wù)器可以訪問(wèn)同一個(gè)數(shù)據(jù)庫(kù),那么會(huì)話數(shù)據(jù)就會(huì)鏡像到所有web服務(wù)器铐刘。具體方法便是通過(guò)向session_set_save_handler()提供一個(gè)實(shí)現(xiàn)SessionHandlerInterface接口的實(shí)例陪每,來(lái)注冊(cè)自定義會(huì)話存儲(chǔ)函數(shù)(在PHP 5.4以后的版本才能這樣用)。首先我們實(shí)現(xiàn)接口如下镰吵,其文件名為db.php檩禾,它使用PDO將session 數(shù)據(jù)存儲(chǔ)在一個(gè)數(shù)據(jù)庫(kù)表中:

class DBHandler implements SessionHandlerInterface {
    protected $dbh;
    /**
    * open 回調(diào)函數(shù)類(lèi)似于類(lèi)的構(gòu)造函數(shù),在會(huì)話打開(kāi)的時(shí)候會(huì)被調(diào)用捡遍。 
    * 這是自動(dòng)開(kāi)始會(huì)話或者通過(guò)調(diào)用session_start() 手動(dòng)開(kāi)始會(huì)話 之后第一個(gè)被調(diào)用的回調(diào)函數(shù)锌订。 
    * 此回調(diào)函數(shù)操作成功返回 TRUE,反之返回 FALSE画株。
    */
    public function open($save_path, $name) {
        try {
            $this->connect($save_path, $name);
            return true;
        } catch (PDOException $e) {
            return false;
        }
    }

    /**
    * close 回調(diào)函數(shù)類(lèi)似于類(lèi)的析構(gòu)函數(shù)辆飘。在 write 回調(diào)函數(shù)調(diào)用之后調(diào)用。
    * 當(dāng)調(diào)用 session_write_close() 函數(shù)之后谓传,也會(huì)調(diào)用 close 回調(diào)函數(shù)蜈项。
    * 此回調(diào)函數(shù)操作成功返回 TRUE,反之返回 FALSE续挟。
    */
    public function close() {
        return true;
    }

    /**
    * 銷(xiāo)毀session時(shí)會(huì)調(diào)用
    * 當(dāng)調(diào)用session_destroy()函數(shù)紧卒,或者調(diào)用session_regenerate_id()函數(shù)并且設(shè)置 destroy 參數(shù)為 TRUE 時(shí),會(huì)調(diào)用此回調(diào)函數(shù)诗祸。
    * 此回調(diào)函數(shù)操作成功返回 TRUE跑芳,反之返回 FALSE。
    */
    public function destroy($session_id) {
        $sth = $this->dbh->prepare("DELETE FROM sessions WHERE session_id = ?");
        $sth->execute(array($session_id));
        return true;
    }

    /**
    * 讀取session時(shí)調(diào)用
    * 如果會(huì)話中有數(shù)據(jù)直颅,read 回調(diào)函數(shù)必須返回將會(huì)話數(shù)據(jù)編碼(序列化)后的字符串博个。如果會(huì)話中沒(méi)有數(shù)據(jù),read 回調(diào)函數(shù)返回空字符串功偿。
    * 
    * 在自動(dòng)開(kāi)始會(huì)話或者通過(guò)調(diào)用 session_start() 函數(shù)手動(dòng)開(kāi)始會(huì)話之后盆佣,PHP內(nèi)部調(diào)用 read 回調(diào)函數(shù)來(lái)獲取會(huì)話數(shù)據(jù)。在調(diào)用 read 之前,PHP會(huì)調(diào)用 open 回調(diào)函數(shù)共耍。
    * 
    * read 回調(diào)返回的序列化之后的字符串格式必須與 write 回調(diào)函數(shù)保存數(shù)據(jù)時(shí)的格式完全一致虑灰。 PHP 會(huì)自動(dòng)反序列化返回的字符串并填充 $_SESSION 超級(jí)全局變量。 雖然數(shù)據(jù)看起來(lái)和 serialize() 函數(shù)很相似痹兜, 但是需要提醒的是穆咐,它們是不同的。
    */
    public function read($session_id) {
        $sth = $this->dbh->prepare("SELECT session_data FROM sessions WHERE session_id = ?");
        $sth->execute(array($session_id));
        $rows = $sth->fetchAll(PDO::FETCH_NUM);
        if (count($rows) == 0) {
            return '';
        } else {
            return $rows[0][0];
        }
    }

    /**
    * 向數(shù)據(jù)庫(kù)中寫(xiě)入數(shù)據(jù)
    */
    public function write($session_id, $session_data) {
        $now = time();
        $sth = $this->dbh->prepare("UPDATE sessions SET session_data = ?,last_update = ? WHERE session_id = ?");
        $sth->execute(array($session_data, $now, $session_id));
        if ($sth->rowCount() == 0) {
            $sth2 = $this->dbh->prepare('INSERT INTO sessions (session_id,session_data, last_update)            VALUES (?,?,?)');
            $sth2->execute(array($session_id, $session_data, $now));
        }
    }

    /**
    * 建表
    */
    public function createTable($save_path, $name, $connect = true) {
        if ($connect) {
            $this->connect($save_path, $name);
        }
        $sql=<<<_SQL_
CREATE TABLE sessions (
session_id VARCHAR(64) NOT NULL,
session_data MEDIUMTEXT NOT NULL,
last_update TIMESTAMP NOT NULL,
PRIMARY KEY (session_id)
)
_SQL_;
        $this->dbh->exec($sql);
    }

    /**
    * 連接數(shù)據(jù)庫(kù)
    */
    protected function connect($save_path) {
        /* 在DSN中查找作為“查詢字符串”參數(shù)的用戶和密碼 */
        $parts = parse_url($save_path);
        if (isset($parts['query'])) {
            parse_str($parts['query'], $query);
            $user = isset($query['user']) ? $query['user'] : null;
            $password = isset($query['password']) ? $query['password'] : null;
            $dsn = $parts['scheme'] . ':';
            if (isset($parts['host'])) {
            $dsn .= '//' . $parts['host'];
            }
            $dsn .= $parts['path'];
            $this->dbh = new PDO($dsn, $user, $password);
        } else {
            $this->dbh = new PDO($save_path);
        }
        $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        // 創(chuàng)建會(huì)話表的方法(使用異常處理)
        try {
            $this->dbh->query('SELECT 1 FROM sessions LIMIT 1');
        } catch (Exception $e) {
            $this->createTable($save_path, NULL, false);
        }
    }
}

接下來(lái)演示如何將該類(lèi)與session_set_save_handler()結(jié)合佃蚜,實(shí)現(xiàn)在數(shù)據(jù)庫(kù)中存儲(chǔ)session數(shù)據(jù)庸娱。

include __DIR__ . '/db.php';
ini_set('session.save_path', 'sqlite:/tmp/sessions.db');
session_set_save_handler(new DBHandler);
session_start();
if (! isset($_SESSION['visits'])) {
    $_SESSION['visits'] = 0;
}
$_SESSION['visits']++;
print 'You have visited here '.$_SESSION['visits'].' times.';

這個(gè)代碼塊假設(shè)與db.php在同一目錄中,一旦將session.save_path設(shè)置為指定的PDO DSN谐算,只需要session_set_save_handler(new DBHandler);就可以將PHP與這個(gè)程序關(guān)聯(lián)起來(lái)熟尉。在此之后,使用會(huì)話的代碼與使用PHP默認(rèn)處理程序的代碼是一樣的洲脂。

關(guān)于以上的函數(shù)講的并不全面斤儿,推薦到 http://php.net/ 去查看詳情。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恐锦,一起剝皮案震驚了整個(gè)濱河市往果,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌一铅,老刑警劉巖陕贮,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異潘飘,居然都是意外死亡肮之,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)卜录,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)戈擒,“玉大人,你說(shuō)我怎么就攤上這事艰毒】鸶撸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵丑瞧,是天一觀的道長(zhǎng)柑土。 經(jīng)常有香客問(wèn)我,道長(zhǎng)绊汹,這世上最難降的妖魔是什么冰单? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮灸促,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己浴栽,他們只是感情好荒叼,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著典鸡,像睡著了一般被廓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萝玷,一...
    開(kāi)封第一講書(shū)人閱讀 49,829評(píng)論 1 290
  • 那天嫁乘,我揣著相機(jī)與錄音,去河邊找鬼球碉。 笑死蜓斧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的睁冬。 我是一名探鬼主播挎春,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼豆拨!你這毒婦竟也來(lái)了直奋?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤施禾,失蹤者是張志新(化名)和其女友劉穎脚线,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體弥搞,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡邮绿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拓巧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斯碌。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肛度,靈堂內(nèi)的尸體忽然破棺而出傻唾,到底是詐尸還是另有隱情,我是刑警寧澤承耿,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布冠骄,位于F島的核電站,受9級(jí)特大地震影響加袋,放射性物質(zhì)發(fā)生泄漏凛辣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一职烧、第九天 我趴在偏房一處隱蔽的房頂上張望扁誓。 院中可真熱鬧防泵,春花似錦、人聲如沸蝗敢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寿谴。三九已至锁右,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間讶泰,已是汗流浹背咏瑟。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痪署,地道東北人码泞。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像惠桃,于是被迫代替她去往敵國(guó)和親浦夷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容