由於 HTTP 是一種「無狀態(stateless)」協定,也就是說,client端每次對server發出的請求都是獨立的,server並不會記住client所發出的每個請求的歷史資訊,因此必須靠其他機制來克服 HTTP 協議的無狀態性,來使某個域名下的所有網頁都能共享這些資訊。除了用 POST、GET 等方式在網頁間傳送資料外,session、cookie和token也是在網路應用程式中經常使用來儲存使用者資料的機制。
例如記住使用者是否已經登入等資訊,就是透過cookie、session和token等來傳送登入憑據,如此一來使用者就不必在每次要跳到下個頁面的時候都得重新登入一次。
那為何不直接記住使用者帳密就好?為什麼還需要設計一組『能代表使用者』的識別碼來取代帳密呢?
最主要的原因就是,很多人在各個網站使用的帳密可能都是同一組,如果記下使用者帳密作為身份代表,只要帳密被有心人士拿到,他不僅能登入這個應用程式,也可能拿這組帳密去登入該使用者在其他應用程式的帳號。
除非請使用者重新設定一次,否則這組身份識別資料是永久有效的。
而使用 session token 來作為登入憑據則可設定其期效,過期後可以作廢再生成新的,就算真的被有心人士偷走,也只有暫時的風險。
由於 cookie 是存在 client 端,所以在不同程式之間的傳遞都可以藉由使用者本身的檔案來存取所需的資訊,但 session 是儲存在 server 端,除非使用者告訴 Server Session 資訊檔案名稱是什麼,不然是無法傳遞「session 值」(就是 sess_後面加的 32 字元), 因此一般來說,會利用兩種方式來傳遞「 session 值 」:
client端訪問server端的流程:
Set-cookie:value[; expires=date][; domain-domain][; path=path][; secure]
session是可能將使用者資訊儲存在server端的資料庫、Memory暫存檔中,端看實作方式。
而cookie則將資訊儲存在client端,通常存在瀏覽器,或local storage,也就是使用者自己的電腦,儲存的位置一般是放在:
實際的作法大多會使用session+cookie,其實要單獨只用其中一個理論上也是行得通,但因種種原因,通常不會單獨只使用session或只使用cookie。
只用session的話,只會在client端儲存一個id,而實際上大部分的資訊都是存在server端;
(大多session的實作,會將session id存在會話cookie中,當使用者關閉瀏覽器,session id也會不見;但若cookie是存在硬碟中,則再次開啟瀏覽器時,之前存的資料還是會存在)
相反的,如果資訊大多存放在cookies(例JWT),一來資訊量大的時候client端可能沒有足夠的空間來儲存,二來因為所有資訊都存在client端,一旦你的電腦被挾持,所有資訊都有可能曝光。而且隨著client端的資訊量變大,網路傳輸的數據量也會變大。
因為 cookie 常常用來存取使用者的資訊,為了怕被拿來濫用,或是佔用太多硬碟空間,
所以對 cookie 做出了以下的限制:
token是當使用者登入成功後,server端產生並回傳的一組能象徵該使用者的『權杖』。
實作方式有很多種,端看server端怎麼寫。
舉個例子:
token由uid+time+sign[+固定參數]構成:
uid: 使用者具唯一性的身份辨識碼
time: 當前的時間戳記
sign: 使用hash/encrypt壓縮成長度固定的16進制string,以防止第三方惡意拼湊。
固定參數(可有可無):將一些常用的固定參數加到token中,以避免重複查詢。
傳統的身份驗證方式,是將token在client端存放於cookie(localStorage, 瀏覽器)中。在server端則存放於資料庫中。
以會員登入流程為例:
在PHP中,可以用$_SESSION 這個array來存取session資料。
session儲存的位置是依照 php.ini 的設定。
每支檔案中若有使用到session都要在最開頭加入session_start();以啟用session
這個變數其實在上回的留言板實做中就有出現過,以下是擷取view.php其中一段程式碼:
<?php
// 啟用 session
session_start();
include "db.php";
// 查詢 guestbook 資料
$sql = "SELECT * FROM guestbook";
$result = mysqli_query($db, $sql);
// 取得使用者名稱並存入 session
$name = isset($_GET['name']) ? htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') : '';
$_SESSION['name'] = $name;
?>
delete.php
<?php
session_start();
include "db.php";
if (!isset($_SESSION['no']) || !isset($_SESSION['name'])) {
die("Session expired or invalid.");
}
$no = (int)$_SESSION['no'];
$name = $_SESSION['name'];
// 使用預備語句避免 SQL Injection
$stmt = $db->prepare("DELETE FROM guestbook WHERE no = ?");
$stmt->bind_param("i", $no);
$success = $stmt->execute();
if (!$success) {
die("Database error: " . $stmt->error);
}
// 跳轉到下一頁時帶著使用者名稱的參數)
header("Location: view.php?name=" . urlencode($name) . "&no=" . urlencode($no));
exit;
?>