منبع اصلی نوشتار زیر در این لینک قرار دارد

ذخیره جلسات در پایگاه داده

دفعه قبل، درباره دزدیدن جلسه صحبت کردم، هنوز منتشر نشده که فیدبک رو ببینم)‌ به هر صورت، این دفعه میخوام روشی رو توضیح بدم که میشه مشکل ذخیره جلسه رو در فایل سیستم حل کرد.

این راه حل آخرین راه حل در این زمینست. اگه تنظیمات سرور درست نباشه، و یه کاربر دیگه روی سیستم شما، بتونه فایلهاتون رو از طریق کاربر خودش باز کنه، خیلی راحت میتونه سورس شما رو بدزده،‌ به دیتابیس شما دسترسی پیدا کنه و … (تنظیمات دیتابیس رو توی PHP معمولا توی فایل متنی مینویسن، خوب اگه یکی اونو باز کنه و بخونه دیتابیس هم امن نیست :) ) اگه میخواید برنامه خودتون رو منتشر کنید و نمیخواید دیگران به سورسش دسترسی داشته باشن، حتما با یه چیزی مثل Zend Guard از اون محافظت کنید، و از اون مهمتر،‌قیمت مهمترین فاکتور خرید هاست نیست!!!! فکر میکنم اعتماد و امنیت مهمتر باشه!

شاید بد نباشه که اول راه حل ساده تر رو نشون بدیم.
اولین مساله، اینکه مشخص کنیم آیا واقعا از آخرین دسترسی به این جلسه، یه مدت زمان خاص گذشته یا نه؟ دوم اینکه مشخص کنیم این جلسه واقعا برای همین کاربریه که الان میخواد ازش استفاده کنه؟
این کد رو ببینید :

<?php
	//session_save_path("../tmp");
	session_start();

	//Settings
	$diff=10*60; //600 Sec time out
	$salt="this-is-uniq-hash-for-any-program";

	$_ip=isset ($_SERVER['HTTP_CLIENT_IP'])?
				$_SERVER['HTTP_CLIENT_IP'] : "UNKNOWN";
	$_ip.=isset ($_SERVER['HTTP_X_FORWARDED_FOR'])?
				$_SERVER['HTTP_X_FORWARDED_FOR'] : "UNKNOWN";
	$_ip.=isset ($_SERVER['REMOTE_ADDR'])?
				$_SERVER['REMOTE_ADDR'] : "UNKNOWN";

	$_agent = isset ($_SERVER['HTTP_USER_AGENT']) ?
				$_SERVER['HTTP_USER_AGENT'] : 'NO USER AGENT';

	$browser_data=$salt.$_ip.$_agent;
	$browser_hash=md5($browser_data);

	$now=time();

	if (isset($_SESSION['last_time']) && isset($_SESSION['browser_hash'])){
		if (strcasecmp($browser_hash,$_SESSION['browser_hash'])!=0
			|| $now-$_SESSION['last_time']>$diff){
			foreach ($_SESSION as $key => $value){
				unset($_SESSION[$key]);
			}
			session_destroy();
			//You can pass to login page or whatever :-"
			header("Location: ".$_SERVER['PHP_SELF']);
			die();	//THIS IS IMPORTANT, never trust user browser,
		}
	}else{
		//this is new session....

		//Old session but without time and browser hash ?
		//Just destroy it, if any, to prevent hijaking
		foreach ($_SESSION as $key => $value){
			unset($_SESSION[$key]);
		}

		$_SESSION['last_time']=$now;
		$_SESSION['browser_hash']=$browser_hash;
	}

این کد رو دقیقا در ابتدای برنامه بگذارید. دقیقا در ابتدای فایل index.php (معمولا) میتونید یه فایل جدا درست کنید این کد رو توش بنویسید و در ابتدای برنامه اینو include کنید. حالا دیگه هم زمان کنترل میشه هم بروزر، گرچه این متد بسیار بسیار سادست :) ولی خیلی هم موثره.
دقیقا اولین خط کد، که کامنت هم شده،‌میتونه این امکان رو به شما بده که جای ذخیره شدن جلسات رو عوض کنید، مثلا بیاریدشون روی پوشه های خودتون(که البته باید یه فولدری باشه که وب سرور بهش دسترسی نداشته باشه، یعنی یا با htaccess یا با هر ابزار دیگه ای کاملا قفلش کرده باشید یا اینکه اصلا خارج از پوشه ریشه باشه. البته این پوشه باید توسط PHP قابل نوشتن باشه که بتونه جلسات رو اونجا ذخیره کنه. اگر نه یکی از بیرون و به راحتی از طریق Browser میتونه فایل رو ببینه)

اما راه دوم، که به شما این امکان رو میده که مثلا جلسات رو توی دیتابیس ذخیره کنید، نه توی فایل. از PDO برای دیتابیس استفاده شده.

برای این روش PHP یه تابع پیش بینی کرده. تابع session_set_save_handler که به شما امکان میده که خودتون ذخیره و بازیابی جلسه رو انجام بدید. من اینجا یه سیستم ساده ایجاد میکنم برای کمترین نیازها (البته با قابلیتهایی که کد بالایی هم داره)
تابعی که گفتم، به عنوان آرگومان ۶ تا callback میگیره، callback نوع خاصی نیست، بلکه اسم توابع دیگه به صورت رشته هستش، میتونه به صورت آرایه باشه اونم در صورتی که بخواید به یک متد از یک کلاس اشاره کنید.
مثلا اگه ما یه تابع داشته باشیم به اسم save ، اونوقت رشته ای مثل “save” که دقیقا اسم این تابع هست رو بهش میگیم callback، حالا اگه این تابع مثلا از کلاس X باشه بخوایم مثلا از یه شیئ به اسم $object اسمشو بنویسیم میشه

array(&$object,"save")

این یه چیزیه معادل اشاره گر به توابع منتها توی PHP دیگه یه نوع جدید معرفی نشده و با همون رشته و آرایه مساله فیصله داده شده.
خوب، برگردیم به تابعی که گفتیم، این تابع ۶ تا آرگومان از جنس callback میگیره، و در زمانهای خاص این ۶ تا رو فراخوانی میکنه.
اول یه جدول درست کنید به اسم sessions با این ساختار :‌

CREATE TABLE IF NOT EXISTS <code>sessions</code> (
  <code>id</code> varchar(100) NOT NULL,
  <code>data</code> text NOT NULL,
  <code>modified</code> datetime NOT NULL,
  <code>hash</code> varchar(60) NOT NULL,
  UNIQUE KEY <code>id</code> (<code>id</code>)
)

حالا باید ۶ تا تابع نوشته بشه، من بنا به دلایلی (منجمله اینکه پیوستگی بیشتر میشه) از یه کلاس استفاده میکنم. یعنی به جای ۶ تا تابع مجزا، ۶ تا تابع مینویسم داخل یک کلاس. ساختار اصلی کلاس، اینه :

class MySessionHandler{
	private $time_out; //Time out for session
	private $salt;//Salt, an uniq string

	private $pdo;//PDO object
	private $browser_hash;//Browser hash

متغیرهاش رو که شما اینجا می ببینید، سه تای اولی وقت ساختن به کلاس پاس داده میشه ولی چهارمی، توی کلاس ساخته میشه. متد سازنده کلاس رو آخر سر مینویسم. حالا میرسیم به توابع مربوط به جلسه. نکته مهم اینه که این توابع باید همه به صورت public باشن.

Open function
‌تابع اول، وقتی فراخوانی میشه که قراره جلسه آغاز بشه. اسم جلسه و آدرس ذخیره شدن اون بهش پاس داده میشه، آدرس رو که اصلا نیازی نداریم چرا که ما میخوایم توی دیتابیس بنویسیم نه توی فایل. اسم جلسه هم مهم نیست :)‌ دست کم من استفاده ای تو این کلاس براش ندیدم، شما میتونید قبل از آغاز جلسه، این اسم رو با کمک تابع session_name عوض کنید، که توصیه میکنم حتما اینکار رو انجام بدید. پیشفرضش PHPSESSID هستش. تابع میشه یه چیزی مثل این :

	public function _my_session_open($save_path, $session_name){
		//Initialize your need here, in my case
		// I need nothing.
		return true;
	}

این تابع برای من کاری قرار نیست بکنه، فقط اینکه نشون میده جلسه شروع شد.
Close function
این تابع برعکس قبلیه. گزارش میده که جلسه تموم شد. بازم برای من مهم نیست. البته این تابع آرگومان نداره. اینطوری میشه :

	public function _my_session_close(){
		//Just deinitialize your resources
		return true;
	}

Write function
خوب رسیدیم به جای مهم، نوشتن . یعنی وقتی جلسه میخواد نوشته بشه. البته PHP یه جا کل اطلاعات جلسه رو میفرسته به این تابع، نه یکی به یکی، به عبارتی تو آخر هر جلسه فقط یه بار این متد فراخوانی میشه وتمام.(دقیقا قبل از تابع close که بالاتر نوشتم)
فرض میکنیم browser_hash قبلا محاسبه شده. اینم بدنه تابع :

	public function _my_session_write($id,$data){
		$safe_id=$this->pdo->quote($id);
		$safe_data=$this->pdo->quote($data);
		$hash=$this->pdo->quote($this->browser_hash);
		$now=date("Y-m-d H:i:s");
		$query="INSERT INTO sessions (id,data,modified,hash) VALUES ($safe_id,$safe_data,'$now',$hash)
			ON DUPLICATE KEY UPDATE data=$safe_data, modified='$now',hash=$hash";
		try{
			$this->pdo->query($query);
		}catch(PDOException $e){
			return false;
		}
		return true;
	}

تابع دو تا آرگومان داره. اولی کلید جلسه هستش، دومی هم اطلاعات کل جلسه. ما باید اینو توی دیتابیس بنویسیم. فیلد id رو من به صورت یکتا تعریف کردم، و با توجه به اینکه هر بار و با هر refresh این تابع یه بار باید اطلاعات رو آپدیت کنه،‌ یا باید اول سطر قبلی رو پاک کنیم و سطر جدید رو بنویسیم، یا اینکه نه، خیلی ساده از کوئری INSERT … ON DUPLICAE KEY UPDATE استفاده کنیم. این کوئری خیلی کار رو راحت میکنه. اگه INSERT با خطای DUPLICATE KEY روبرو بشه، UPDATE رو انجام میده.
دقت کنید که من زمان و اطلاعات بروزر رو هم همزمان توی دیتابیس قرار دادم.

Read function
وقت برداشته :)‌ همه اینها واسه همین تابع هستش. این تابع هم فقط یکبار در ابتدای جلسه و بعد از تابع open فراخوانی میشه. آرگومانش فقط کلید جلسه هستش و باید محتوای جلسه به عنوان بازگشتی برگرده. فقط کافیه که اگه جلسه وجود داره، محتوایی که توی write نوشته شده برگرده اگه وجود نداره یا زمانش گذشته یا بروزرش مشکل داره فقط مقدار خالی رو برگردونه. تابع میشه یه چیزی مثل این :

	public function _my_session_read($id){
		$query="SELECT * FROM sessions WHERE id= ".$this->pdo->quote($id);
		try{
			$sth=$this->pdo->query($query);
		}catch(PDOException $e){
			return "";
		}
		//Is there any???
		if ($sth->rowCount()!=1)
			return "";
		$data=$sth->fetch();
		//Now its time to validate ...
		$pdo_time=strtotime($data['modified']);
		$pdo_hash=$data['hash'];

		//Check for time out and browser data
		if (time()-$pdo_time>$this->time_out || strcasecmp($this->browser_hash,$pdo_hash)!=0)
			return "";

		//anything is ok, return data
		return $data['data'];

	}

تابع خیلی سادست. اطلاعات رو میخونه، و بررسی میکنه که آیا اصلا اطلاعاتی هست؟ یا اینکه زمانش درسته؟ بروزرش درسته؟ و اگه همه درست بود محتوا رو پس میده.
Destroy function
این تابع فقط زمانی فراخوانی میشه که شما تابع session_destroy رو فراخوانی کنید (مثلا با خروج کاربر بهتره این تابع فراخوانی بشه) کلید جلسه بهش ارسال میشه و انتظار داره شما اون جلسه رو از بین ببرید. سادست :

	public function _my_session_destroy($id){
		$safe_id=$this->pdo->quote($id);
		$query="DELETE FROM sessions WHERE id=$safe_id";
		echo $query;
		try{
			$this->pdo->query($query);
		}catch(PDOException $e){
			return false;
		}
		//$this->_my_session_gc($this->time_out);
		return true;
	}

GC function
این تابع، قاعدتا باید هر از چند گاهی یک بار توسط PHP فراخوانی بشه، ولی تو تمام تست های من توی ۵-۶ سال گذشته حتی یکبار هم PHP اونرو فراخوانی نکرده :) ولی خوب من خودم معمولا کد destroy رو یه کم تغییر میدم و آخرش خودم تابع GC رو هم فراخوانی میکنم(خط کامنت شده تابع بالایی). شما هم مختارید که هر کاری دوست دارید انجام بدید. اینم بدنه تابع :

	public function _my_session_gc($maxlifetime){
		//You can use your timeout instead of this.
		$date=time()-$maxlifetime;
		$date_str=date("Y-m-d H:i:s",$date);
		$query="DELETE FROM sessions WHERE modified<$date";
		try{
			$this->pdo->query($query);
		}catch(PDOException $e){
			return false;
		}
		return true;
	}

GC مخفف Garbage Collector میشه، یه چیزی تو مایه های آشغال جمع کن.
آرگومانش، از تنظیمات PHP هستش، زمانی که توی فایل PHP.ini قابل تغییره، ولی گفتم که، طبق تجربیات من اصولا این تابع فراخوانی نمیشه!!

اما حالا میرسیم به تابع سازنده کلاس که قراره همه کارها رو انجام میده :

	function __construct(&$pdo,$time_out=600,$salt="change this in your code"){
		if (!$pdo)
			die("PDO must created.");
		$this->pdo=$pdo;
		$this->time_out=$time_out;
		$this->salt=$salt;
		$this->_calcHahs();

		session_set_save_handler(array(&$this,"_my_session_open"),
								 array(&$this,"_my_session_close"),
								 array(&$this,"_my_session_read"),
								 array(&$this,"_my_session_write"),
								 array(&$this,"_my_session_destroy"),
								 array(&$this,"_my_session_gc")
								 );

	}

	private function _calcHahs(){
		$_ip=isset ($_SERVER['HTTP_CLIENT_IP'])?
					$_SERVER['HTTP_CLIENT_IP'] : "UNKNOWN";
		$_ip.=isset ($_SERVER['HTTP_X_FORWARDED_FOR'])?
					$_SERVER['HTTP_X_FORWARDED_FOR'] : "UNKNOWN";
		$_ip.=isset ($_SERVER['REMOTE_ADDR'])?
					$_SERVER['REMOTE_ADDR'] : "UNKNOWN";

		$_agent = isset ($_SERVER['HTTP_USER_AGENT']) ?
					$_SERVER['HTTP_USER_AGENT'] : 'NO USER AGENT';

		$browser_data=$this->salt.$_ip.$_agent;
		$this->browser_hash=md5($browser_data);
	}

تابع _calcHash هم که برای محاسبه hash مربوط به IP و Browser هستش.

اما نکته‌‍ی مهم آخر، حتما، حتما و حتما در آغاز همه کدهای PHP خودتون،‌ اینکار رو انجام بدید، اگر نه این کد درست کار نمیکنه :

	date_default_timezone_set("Asia/Tehran");

البته TimeZone های دیگه رو هم میتونید اینجا ببینید. اینطوری زمان تمام توابع با زمان ایران (یا هر جا شما دوست دارید) تنظیم میشه، خصوصا کار ما که زمان توش خیلی مهمه. طرز استفاده از این کد خیلی راحته :

	date_default_timezone_set("Asia/Tehran");
	try{
		$pdo=new PDO('mysql:dbname=test;host=127.0.0.1','root','bita123');
	}catch (PDOException $e){
		echo "failed : " . $e->getMessage();
		die();
	}
	//Default php time out is :
	//$time_out=session_cache_expire()*60;
	$time_out=60*10; //600 sec
	$session=new MySessionHandler($pdo,$time_out,"some-uniq-per-site-string");
	//Now its ok to play with session :)
	session_start();
	if (isset($_SESSION["test"]))
		echo $_SESSION["test"];
	else{
		echo "test is not defined";
		$_SESSION["test"]="Hello, I'm test session's value!";
	}

فقط یه نسخه از این کلاس بسازید و بعد جلسه رو شروع کنید. والسلام. حالا جلسات توی دیتابیس ذخیره میشن که خیلی امنتر از فایله. البته اگه اونم …. بی خیال بابا!!

–چقدر نوشتم…..
اینم کد کلاس به صورت کامل



برچسب ها : , , , , ,