PHP Session
Session lifecycle, reading and writing $_SESSION, and defending against session fixation.
Starting a session
session_start() must precede output. It reads the session ID from the request, creating one and sending Set-Cookie if absent.
<?php
session_start();
$_SESSION["user"] = "Tom";
$_SESSION["cart"] = ["apple" => 2, "pear" => 1];
echo session_status() === PHP_SESSION_ACTIVE ? "会话已建立" : "无会话", "\n";
echo $_SESSION["user"], "\n";
echo count($_SESSION["cart"]), "\n";
unset($_SESSION["cart"]);
echo isset($_SESSION["cart"]) ? "有购物车" : "购物车已清空", "\n";
session_destroy();
echo session_status() === PHP_SESSION_ACTIVE ? "仍活跃" : "已销毁", "\n";会话已建立
Tom
2
购物车已清空
已销毁Session vs cookie
| Aspect | Cookie | Session |
|---|---|---|
| Where stored | 用户浏览器 | 服务器 |
| Size limit | 约 4KB | 受内存/存储限制 |
| Tamper-proof | 否 | 是(数据在服务端) |
| Lifetime | 可长期保存 | 默认浏览器关闭即失效 |
| Scalability | 天然支持多机 | 多机需共享存储 |
| Good for | 偏好、记住我 token | 登录状态、购物车 |
Login state
<?php
session_start();
// 登录成功后
function login(string $user): void {
session_regenerate_id(true); // 换新 ID,防会话固定
$_SESSION["user"] = $user;
$_SESSION["login_at"] = 1700000000;
}
function logout(): void {
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$p = session_get_cookie_params();
setcookie(session_name(), "", time() - 42000, $p["path"], $p["domain"], $p["secure"], $p["httponly"]);
}
session_destroy();
}
function currentUser(): ?string {
return $_SESSION["user"] ?? null;
}
login("Tom");
echo currentUser(), "\n";
echo count($_SESSION), "\n";Tom
2session_regenerate_id(true) defeats session fixation: an attacker plants their own ID, the victim logs in, and they share the session. Regenerating on login removes the premise.Where sessions live
By default each session is a file in the temp directory. With multiple servers the files are not shared, so users lose their login when routed elsewhere.
<?php
// 用 Redis 存会话(需装 redis 扩展)
ini_set("session.save_handler", "redis");
ini_set("session.save_path", "tcp://127.0.0.1:6379");
// 或者用数据库
// 自己实现 SessionHandlerInterface 再 session_set_save_handler()
echo ini_get("session.save_handler"), "\n";
echo ini_get("session.save_path"), "\n";redis
tcp://127.0.0.1:6379Production almost always uses Redis or a database. File-based sessions suit only small single-server apps.
Session security checklist
- Regenerate on login —— defeats fixation
- **Set
httponly,secure,samesite=Lax** —— against XSS theft and CSRF - **
session.cookie_lifetime = 0** —— session cookie not persisted - **
session.gc_maxlifetime** —— idle timeout, default 1440s - Clear on logout —— empty the array, destroy, delete the cookie
session_destroy() alone leaves $_SESSION populated for this request; clearing the array alone leaves the cookie. Do all three.Flash messages
One-shot notices like "saved successfully" live in the session and are removed after reading — the standard PRG companion.
<?php
session_start();
function flash(string $key, ?string $value = null): ?string {
if ($value !== null) {
$_SESSION["_flash"][$key] = $value;
return null;
}
$v = $_SESSION["_flash"][$key] ?? null;
unset($_SESSION["_flash"][$key]);
return $v;
}
flash("ok", "保存成功");
echo flash("ok") ?? "(空)", "\n";
echo flash("ok") ?? "(第二次读不到了)", "\n";保存成功
(第二次读不到了)开启会话
session_start() 必须在输出之前调用。它读请求里的会话 ID,没有就新建一个并发 Set-Cookie。
<?php
session_start();
$_SESSION["user"] = "Tom";
$_SESSION["cart"] = ["apple" => 2, "pear" => 1];
echo session_status() === PHP_SESSION_ACTIVE ? "会话已建立" : "无会话", "\n";
echo $_SESSION["user"], "\n";
echo count($_SESSION["cart"]), "\n";
unset($_SESSION["cart"]);
echo isset($_SESSION["cart"]) ? "有购物车" : "购物车已清空", "\n";
session_destroy();
echo session_status() === PHP_SESSION_ACTIVE ? "仍活跃" : "已销毁", "\n";会话已建立
Tom
2
购物车已清空
已销毁会话 vs Cookie
| 对比项 | Cookie | Session |
|---|---|---|
| 存储位置 | 用户浏览器 | 服务器 |
| 大小限制 | 约 4KB | 受内存/存储限制 |
| 防篡改 | 否 | 是(数据在服务端) |
| 生命周期 | 可长期保存 | 默认浏览器关闭即失效 |
| 扩展性 | 天然支持多机 | 多机需共享存储 |
| 适合存 | 偏好、记住我 token | 登录状态、购物车 |
登录状态
<?php
session_start();
// 登录成功后
function login(string $user): void {
session_regenerate_id(true); // 换新 ID,防会话固定
$_SESSION["user"] = $user;
$_SESSION["login_at"] = 1700000000;
}
function logout(): void {
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$p = session_get_cookie_params();
setcookie(session_name(), "", time() - 42000, $p["path"], $p["domain"], $p["secure"], $p["httponly"]);
}
session_destroy();
}
function currentUser(): ?string {
return $_SESSION["user"] ?? null;
}
login("Tom");
echo currentUser(), "\n";
echo count($_SESSION), "\n";Tom
2session_regenerate_id(true) 是防会话固定攻击的关键一步。攻击者先把自己的会话 ID 塞给受害者,受害者登录后攻击者就共用了同一个会话。登录成功时换 ID 能直接废掉这个前提。会话的存储位置
默认存在服务器的临时目录,每个会话一个文件。多台服务器时文件不共享,用户换机器就掉登录。
<?php
// 用 Redis 存会话(需装 redis 扩展)
ini_set("session.save_handler", "redis");
ini_set("session.save_path", "tcp://127.0.0.1:6379");
// 或者用数据库
// 自己实现 SessionHandlerInterface 再 session_set_save_handler()
echo ini_get("session.save_handler"), "\n";
echo ini_get("session.save_path"), "\n";redis
tcp://127.0.0.1:6379生产环境几乎都用 Redis 或数据库存会话。文件方案只适合单机小项目。
会话安全清单
- **登录时
session_regenerate_id(true)** —— 防固定攻击 - **Cookie 设
httponly+secure+samesite=Lax** —— 防 XSS 窃取与 CSRF - **设
session.cookie_lifetime = 0** —— 会话 Cookie 不落盘 - **设
session.gc_maxlifetime** —— 控制空闲超时(默认 1440 秒) - 登出时彻底清空 ——
$_SESSION = []加session_destroy()加删 Cookie
session_destroy() 不清 $_SESSION,当前请求里数组还有值;只清数组不删 Cookie,浏览器下次还会带着旧 ID 回来。三件事都要做。闪存消息
「操作成功」这类提示只显示一次。用会话存一下,读完就删,是 PRG 模式的标准搭配。
<?php
session_start();
function flash(string $key, ?string $value = null): ?string {
if ($value !== null) {
$_SESSION["_flash"][$key] = $value;
return null;
}
$v = $_SESSION["_flash"][$key] ?? null;
unset($_SESSION["_flash"][$key]);
return $v;
}
flash("ok", "保存成功");
echo flash("ok") ?? "(空)", "\n";
echo flash("ok") ?? "(第二次读不到了)", "\n";保存成功
(第二次读不到了)