Session 與 cookie 非常類似,都是用來儲存一些小東西,但最主要的差別在於 session 是儲存在伺服器端。而 session 最常見的用法,是透過自動的跟 cookie 做搭配,來認出現在是誰在使用網頁。若你要在某個 PHP 程式當中使用 session,首先必須呼叫「session_start」,此函數必須在網頁輸出任何東西之前呼叫,就算是多一個空白也不行,否則會產生錯誤。之後,我們可以使用「$_SESSION["名稱"]」的方式去設定與讀取內容。而若要刪除 session,則可使用「unset」函式刪除指定名稱的 session,或者用「session_destroy」刪除所有的 session。以下是一系列的範例,各位可以在建立、讀取、毀滅之間互相點選看看。

創建 session:

<?php
session_start();
$_SESSION["abc"] = "def";
$_SESSION["xyz"] = 123;
?>

Create abc & xyz

讀取 session:

<?php
session_start();
echo '$_SESSION["abc"] is '.$_SESSION["abc"].'<br>';
echo '$_SESSION["xyz"] is '.$_SESSION["xyz"];
?>

用 unset 毀掉 session:

<?php
session_start();
unset($_SESSION["abc"]);
?>

Unset abc

用 destory 毀掉 session:

<?php
session_start();
session_destroy();
?>

Destory

如果你點進「創建 session」的範例後,打開了瀏覽器的開發者工具,可以看到一個名為「PHPSESSID」的 cookie;如果先把這個 cookie 刪掉,再點選「讀取 session」,你會發現讀取失敗,這是因為 session 加上 cookie 的組合,會被用來分辨是哪一個用戶,而刪掉 cookie 之後,伺服器就無法認得你是誰了。Session 本身在伺服器端,則預設是以文件的方式儲存,檔名會是「sess_」加上前述你在「PHPSESSID」那個 cookie 看到的值。關於存放的地點,以 Win 7 為例,若使用 AppServ 預設會儲存在「C:\Users\你的帳號\AppData\Local\Temp」底下,XAMPP 則預設儲存在「C:\xampp\tmp」。它是一個純文字檔,各位可以試著打開看看內容。

因此,運用 session,即可以做出登入登出的功能。以下範例的帳號是 test1 到 test5 ,密碼都是 12345,整張資料表的參考結構如下,其中密碼的部分以 md5 加密。你可以自行設定資料表及新增資料,或者在這裡下載:

登入頁面,就是在通過驗證後,設定 session 變數,然後轉到登入成功的頁面。驗證的基本邏輯是帳號密碼都要符合,細節部分可以自行依需求設計:

<?php
session_start();
include("pdoInc.php");

if(isset($_SESSION['account']) && $_SESSION['account'] != null){ // 如果登入過,則直接轉到登入後頁面
	echo '<meta http-equiv=REFRESH CONTENT=0;url=php_sess_index.php>';
}
else if(isset($_POST['account']) && isset($_POST['password'])){
	$acc = preg_replace("/[^A-Za-z0-9]/", "", $_POST['account']);
	$pwd = preg_replace("/[^A-Za-z0-9]/", "", $_POST['password']);
	if($acc != NULL && $pwd != NULL){
		$sth = $dbh->prepare('SELECT * FROM user where account = ?');
		$sth->execute(array($acc));
		$row = $sth->fetch(PDO::FETCH_ASSOC);
		// 比對密碼
		if($row['pwd'] == md5($pwd)){
			$_SESSION['account'] = $row['account'];
			$_SESSION['nickname'] = $row['nickname'];
			$_SESSION['is_admin'] = $row['is_admin'];
			echo '<meta http-equiv=REFRESH CONTENT=0;url=php_sess_index.php>';
		}
	}
}
?>

<html><head></head>
<body bgcolor="#ccccff">

<form action="php_sess_login.php" method="post">
	帳號:<input type="text" name="account"><br>
	密碼:<input type="text" name="password"><br>
	<input type="submit">
</form>

</body></html>

這是登入成功後的頁面的範例,我們假設排行榜是需要登入才能看的資訊。所有需要登入才能看的頁面,都必須詳加保護,以免被直接輸入網址連入而產生問題:

<?php
session_start();
include("pdoInc.php");

if(!isset($_SESSION['account'])){
	echo '<meta http-equiv=REFRESH CONTENT=0;url=php_sess_login.php>';
}
?>

<html><head></head>
<body bgcolor="#ccccff">

<?php
echo "Hi, ".$_SESSION['account']." (".htmlspecialchars($_SESSION['nickname']).")";
?>

<table border="1">
	<tr>
		<th>資料序號</th>
		<th>當日排名</th>
		<th>前日排名</th>
		<th>歌曲名稱</th>
		<th>演 唱 者</th>
	</tr>
	<?php
		$sql = "SELECT * from songrank";
		$sth = $dbh->query($sql);
		while($row = $sth->fetch(PDO::FETCH_ASSOC)){
			echo "<tr><td>".$row['id']."</td>";
			echo "<td>".$row['this_rank']."</td>";
			echo "<td>".$row['prev_rank']."</td>";
			echo "<td>".$row['song_name']."</td>";
			echo "<td>".$row['singer_name']."</td><tr>";
		}
	?>
</table>

<a href="php_sess_logout.php">登出</a>

</body></html>

事實上,上述範例的保護還不夠詳實,各位知道如何破解及修改嗎?

登出時,毀掉 session 即可:

<?php
session_start();
session_destroy();
echo '<meta http-equiv=REFRESH CONTENT=0;url=php_sess_login.php>';
?>

登入之後,就可以透過網頁跟使用者進行更多的互動。例如更改資料,只要將表單送過來的資料,利用 SQL 的「UPDATE」指令進行更新即可達成,如下:

<?php
session_start();
include("../examples/pdoInc.php");

if(!isset($_SESSION['account'])){
	die('<meta http-equiv=REFRESH CONTENT=0;url=../examples/php_sess_login.php>');
}

$resultStr = '';
if(isset($_POST['nickname']) && isset($_POST['password'])){
	$sth = $dbh->prepare('SELECT account FROM user WHERE account = ? and pwd = md5(?)');
	$sth->execute(array($_SESSION['account'], $_POST['password']));
	if($sth->rowCount() == 1){
		if($_POST['newpwd1'] != '' && $_POST['newpwd2'] != ''){
			if($_POST['newpwd1'] == $_POST['newpwd2']){
				$sth2 =  $dbh->prepare('UPDATE user SET nickname = ?, pwd = md5(?) WHERE account = ?');
				$sth2->execute(array($_POST['nickname'], $_POST['newpwd1'], $_SESSION['account']));
				$resultStr = '修改暱稱或密碼成功';
				$_SESSION['nickname'] = $_POST['nickname'];
			}
			else {
				$resultStr = '兩次新密碼填寫不同';
			}
		}
		else {
			$sth2 =  $dbh->prepare('UPDATE user SET nickname = ? WHERE account = ?');
			$sth2->execute(array($_POST['nickname'], $_SESSION['account']));
			$_SESSION['nickname'] = $_POST['nickname'];
			$resultStr = '修改暱稱成功';
		}
	}
	else {
		$resultStr = '密碼填寫錯誤';
	}
}
?>

<html><head></head>
<body bgcolor="#ccccff">

<?php echo $resultStr;?>

<form action="<?php echo basename($_SERVER['PHP_SELF']);?>" method="POST">
	帳號:<?php echo $_SESSION['account'];?><br>
	暱稱:<input name="nickname" value="<?php echo htmlspecialchars($_SESSION['nickname']);?>"><br>
	密碼:<input name="password" placeholder="必填"><br>
	修改密碼:<input name="newpwd1" placeholder="僅修改密碼時需填"><br>
	確認密碼:<input name="newpwd2" placeholder="僅修改密碼時需填"><br>
	<input type="submit">
</form>

<a href="../examples/php_sess_logout.php">登出</a>

</body></html>

而例如刪除資料,則是正確的指定 DELETE 的條件即可。而實務上,通常會希望只有作者本人,或者擁有特定權限的人才能刪除資料,所以也需要搭配適當的權限控管,以免被任意的使用者刪除資料。此範例是從 PDO 篇的討論版,搭配前述的登入登出修改而來(為了範例簡潔,發表主題的部分已經移除);範例中假設 test1 這個使用者是管理員(is_admin 欄位為 1),有權限可以刪除主題:

<?php
session_start();
include("../examples/pdoInc.php");

if(isset($_GET['del']) && $_SESSION['is_admin'] == 1){
	$sth = $dbh->prepare('DELETE FROM dz_thread WHERE id = ? or root_thread_id = ?');
	$sth->execute(array((int)$_GET['del'], (int)$_GET['del']));
	echo 
		'<meta http-equiv=REFRESH CONTENT=0;url='.
		basename($_SERVER['PHP_SELF']).'?id='.(int)$_GET['id'].'>';
}

?>

<html><head></head>
<body bgcolor="#ccccff">

<?php
	function showThread($row){
		echo 
			'<a href="php_dz_viewThread.php?id='.$row['id'].'">'.
			htmlspecialchars($row['title']).'</a> by '.
			htmlspecialchars($row['nickname']).
			' from '.$row['ip'];
		if($_SESSION['is_admin'] == 1){
			echo
				'<a href="'.
				basename($_SERVER['PHP_SELF']).'?id='.(int)$_GET['id'].'&del='.$row['id'].
				'">刪除</a>';
		}
		echo '<br>';
	}

	$sth = $dbh->prepare('SELECT id FROM dz_board WHERE id = ?');
	$sth->execute(array((int)$_GET['id']));
	if($sth->rowCount() == 1){
		$sth2 = $dbh->prepare('SELECT * from dz_thread WHERE board_id = ? ORDER BY id');
		$sth2->execute(array((int)$_GET['id']));
		while($row = $sth2->fetch(PDO::FETCH_ASSOC)){
			showThread($row);
		}
	}
	else {
		echo '看板不存在';
	}
?>

</body></html>