Session 与 Cookie

Session 和 Cookie 是 Web 开发中实现用户状态保持的两种核心机制。HTTP 协议本身是无状态的,每个请求都是独立的,服务器无法区分不同用户的请求。Cookie 和 Session 正是为了解决这个问题而设计的,它们让服务器能够"记住"用户,实现登录状态、购物车、个性化设置等功能。

Cookie 存储在客户端浏览器,适合保存非敏感的偏好设置。浏览器在每次请求时会自动将 Cookie 附加到请求头中发送给服务器,服务器通过读取 Cookie 来识别用户。

PHP 实例
// 设置 Cookie(必须在任何输出之前调用)
setcookie(
    'username',          // 名称
    '张三',              // 值
    time() + 86400 * 7, // 过期时间(7天后)
    '/',                 // 路径
    '',                  // 域名
    true,                // 仅 HTTPS
    true                 // HttpOnly(JS 无法访问)
);

// 读取 Cookie $username = $_COOKIE['username'] ?? '访客'; echo "你好,$username";

// 删除 Cookie(设置过期时间为过去) setcookie('username', '', time() - 3600, '/'); ?> ``

HttpOnly 标志防止 JavaScript 通过 document.cookie 读取 Cookie,有效防御 XSS 攻击窃取 Cookie。Secure 标志确保 Cookie 只通过 HTTPS 传输,防止中间人攻击。PHP 8.0 起,setcookie() 支持通过关联数组传递选项,语义更清晰:setcookie('name', 'value', ['expires' => time()+3600, 'samesite' => 'Strict'])

Session 基础

Session 数据存储在服务器端,客户端只保存一个 Session ID(通常通过 Cookie 传递)。服务器根据 Session ID 找到对应的会话数据,从而识别用户。这比 Cookie 更安全,因为敏感数据不会暴露在客户端。

`php <?php session_start(); // 必须在页面最顶部调用

// 存储数据 $_SESSION['user_id'] = 1; $_SESSION['username'] = '张三'; $_SESSION['is_admin'] = false;

// 读取数据 echo $_SESSION['username']; // 张三

// 删除单个 unset($_SESSION['is_admin']);

// 销毁整个 Session(退出登录) session_destroy(); ?> `

session_start() 必须在任何输出之前调用(包括空格和换行),否则会报"headers already sent"错误。如果使用 PHP 的输出缓冲(ob_start()),则可以在输出后调用。Session 默认存储在服务器的临时文件中,生产环境可以配置为存储在 Redis 或数据库中,以支持多服务器部署。

登录系统完整示例

下面是一个完整的登录系统实现,包含登录页、仪表盘和退出登录三个文件,展示了 Session 在用户认证中的典型用法。

`php <?php // login.php session_start();

// 已登录则跳转 if (isset($_SESSION['user_id'])) { header('Location: dashboard.php'); exit; }

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? '';

// 查数据库(示例) $pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'root', 'pass'); $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC);

if ($user && password_verify($password, $user['password'])) { // 登录成功:重新生成 Session ID 防止固定攻击 session_regenerate_id(true); $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; header('Location: dashboard.php'); exit; } else { $error = "用户名或密码错误"; } } ?> `

session_regenerate_id(true) 在登录成功后重新生成 Session ID,防止 Session 固定攻击(攻击者预先设置一个已知的 Session ID,诱导用户使用该 ID 登录)。参数 true 表示同时删除旧的 Session 文件。

`php <?php // dashboard.php session_start();

// 验证登录状态 if (!isset($_SESSION['user_id'])) { header('Location: login.php'); exit; }

echo "欢迎," . htmlspecialchars($_SESSION['username']); ?> `

`php <?php // logout.php session_start(); session_unset(); session_destroy();

// 清除 Session Cookie setcookie(session_name(), '', time() - 3600, '/');

header('Location: login.php'); exit; ?> `

退出登录时需要同时调用 session_unset()(清空 $_SESSION 数组)和 session_destroy()(删除服务器端的 Session 文件),并手动清除客户端的 Session Cookie,确保彻底退出。

Session 配置优化

默认的 Session 配置在安全性上有一些不足,生产环境应在 session_start() 之前进行安全加固。

`php <?php // 在 session_start() 之前配置 ini_set('session.cookie_httponly', 1); // 防 XSS ini_set('session.cookie_secure', 1); // 仅 HTTPS ini_set('session.use_strict_mode', 1); // 严格模式 ini_set('session.gc_maxlifetime', 3600); // 1小时过期

session_start(); ?> `

use_strict_mode 开启后,服务器会拒绝客户端提交的未初始化的 Session ID,防止 Session 固定攻击。gc_maxlifetime 控制 Session 数据在服务器端的保留时间,超时后会被垃圾回收机制清理。这些配置也可以在 php.ini.htaccess 中全局设置。

特性CookieSession
存储位置客户端浏览器服务器
安全性低(可被篡改)
容量~4KB无限制
生命周期可自定义浏览器关闭或超时
适用场景记住我、偏好设置登录状态、购物车

闪存消息(Flash Message)

闪存消息是一种"一次性"的 Session 数据,读取后立即删除,常用于表单提交后的成功/失败提示。这种模式避免了刷新页面后重复显示消息的问题。

`php <?php session_start();

// 设置一次性消息 function flash(string $key, string $message = ''): string { if ($message) { $_SESSION['_flash'][$key] = $message; return ''; } $msg = $_SESSION['_flash'][$key] ?? ''; unset($_SESSION['_flash'][$key]); return $msg; }

// 存入 flash('success', '保存成功!');

// 取出(取完即删) echo flash('success'); // 保存成功! echo flash('success'); // 空字符串 ?> `

闪存消息配合 PRG(Post/Redirect/Get)模式使用效果最佳:表单提交后,服务器处理完毕将消息存入 Flash,然后重定向到 GET 页面,GET 页面读取并显示消息。这样即使用户刷新页面,也不会重复提交表单。

记住我功能

"记住我"功能通过长期 Cookie 实现,但不能直接存储用户 ID,应存储一个随机令牌,并在数据库中记录令牌与用户的对应关系。

`php <?php // 登录时,如果勾选了"记住我" if (isset($_POST['remember'])) { $token = bin2hex(random_bytes(32)); // 生成安全随机令牌 $userId = $user['id']; $expire = time() + 86400 * 30; // 30天

// 存入数据库 $pdo->prepare("INSERT INTO remember_tokens (user_id, token, expires_at) VALUES (?, ?, ?)") ->execute([$userId, hash('sha256', $token), date('Y-m-d H:i:s', $expire)]);

// 设置 Cookie(存储原始令牌) setcookie('remember_token', $token, $expire, '/', '', true, true); }

// 每次请求时检查记住我 Cookie if (!isset($_SESSION['user_id']) && isset($_COOKIE['remember_token'])) { $token = $_COOKIE['remember_token']; $stmt = $pdo->prepare("SELECT * FROM remember_tokens WHERE token = ? AND expires_at > NOW()"); $stmt->execute([hash('sha256', $token)]); $record = $stmt->fetch();

if ($record) { session_regenerate_id(true); $_SESSION['user_id'] = $record['user_id']; // 可选:轮换令牌,提高安全性 } }

数据库中存储令牌的哈希值而不是原始值,即使数据库泄露,攻击者也无法直接使用令牌。每次使用后轮换令牌(删除旧令牌,生成新令牌)可以检测令牌盗用。

常见问题

Q1:Session 和 JWT(JSON Web Token)有什么区别,应该如何选择?

A:Session 将状态存储在服务器端,服务器需要维护 Session 存储(文件或 Redis),适合传统 Web 应用;JWT 将状态编码在令牌中,服务器无需存储,适合无状态的 API 和微服务架构。Session 的优势是可以随时失效(删除服务器端数据即可),JWT 一旦签发在过期前无法撤销(除非维护黑名单)。对于需要即时踢出用户(如修改密码后强制重新登录)的场景,Session 更合适。

Q2:多台服务器部署时,Session 如何共享?

A:默认的文件 Session 只存在于单台服务器,多服务器部署时需要集中存储。常见方案:① 使用 Redis 存储 Session(session.save_handler = redis),所有服务器共享同一个 Redis;② 使用数据库存储 Session;③ 使用 Nginx 的 IP Hash 负载均衡,将同一用户的请求固定到同一台服务器(但这会影响负载均衡效果)。推荐方案是 Redis,性能好且支持过期时间。

Q3:如何防止 Session 劫持?

A:Session 劫持是攻击者获取合法用户的 Session ID 后冒充该用户。防御措施:① 开启 session.cookie_httponlysession.cookie_secure,防止 XSS 和中间人攻击窃取 Cookie;② 登录后调用 session_regenerate_id(true) 更换 Session ID;③ 在 Session 中记录用户的 IP 和 User-Agent,每次请求时验证是否一致(但 IP 可能变化,需权衡);④ 使用 HTTPS 加密传输。

Q4:session_destroy()$_SESSION 还能访问吗?

A:session_destroy() 只删除服务器端的 Session 文件,不会清空当前请求中的 $_SESSION 数组。因此在同一请求中,session_destroy() 后仍然可以读取 $_SESSION 的值。要彻底清空,应先调用 session_unset()$_SESSION = [],再调用 session_destroy()`。