第4章 4.2 Cookie 与 Session

「上节课小明的购物车数据为什么刷新就没了?」
——因为购物车信息存在「内存」里,页面一关就丢了。这节课我们给它安个「永久仓库」。


🎯 开场 3 分钟:为什么要学这个?

想象你去超市寄存柜存包:

  • 柜子(Cookie):给你一把钥匙(随机字符串),包存在固定柜子里。你拿着钥匙,下次来直接开柜子取包。钥匙丢了/过期了,柜子就打不开了。
  • 超市管理员(Session):你报会员卡号,管理员翻记录本找你之前存的东西。记录本在管理员手里(服务器端),比柜子更安全。

痛点问题
1. 用户登录后刷新页面,怎么知道「这个浏览器是张三而不是李四」?
2. 购物车里的东西,凭什么关掉浏览器还在?

学完这章,你能写出「记住我登录」「购物车持久化」这种真实功能。


🧱 基础 25 分钟:核心概念

什么是 Cookie?——超市柜子的钥匙

Cookie 是服务器发给浏览器的一小段文本,浏览器会自动存起来,下次请求时再带回去。

生\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n活类比:你去游泳馆,柜子钥匙上写着「3号柜」,这把钥匙就是 Cookie。你下次来出示钥匙,工作人员就知道「3号柜是你的」。

为什么要用 Cookie?
- 解决 HTTP 无状态问题(服务器记不住你是谁)
- 存储用户偏好、登录状态等少量数据

PHP 中操作 Cookie

<?php
// 设置 Cookie:key 是 "username",值是 "xiaoming",1小时后过期
setcookie("username", "xiaoming", time() + 3600);

// 读取 Cookie
if (isset($_COOKIE["username"])) {
echo "欢迎回来," . $_COOKIE["username"];
} else {
echo "你是新用户";
}
  • setcookie("键", "值", 过期时间戳):像往柜子放东西
  • $_COOKIE["键"]:像拿钥匙开柜子取东西

什么是 Session?——游泳馆的会员记录本

Session 是服务器上存储的用户数据,依赖 Cookie 保存一个 session_id 来识别用户。

生活类比:Cookie 是钥匙,Session 是柜子里详细记录你物品的登记本。钥匙只是入口,真正的东西在柜子里(服务器端)。

为什么要用 Session?
- Cookie 存不了敏感信息(用户能看到)
- Session 数据在服务器端,更安全
- 能存更多数据

PHP 中操作 Session

<?php
// 启动 Session(必须在任何输出之前!)
session_start();

// 存储数据
$_SESSION["username"] = "xiaoming";
$_SESSION["role"] = "admin";

// 读取数据
if (isset($_SESSION["username"])) {
echo "当前用户:" . $_SESSION["username"];
echo ",角色:" . $_SESSION["role"];
}

// 删除单个数据
unset($_SESSION["role"]);

// 删除所有数据(登出)
session_destroy();

注意! session_start() 必须放在 PHP 文件第一行(或在任何 echoprint 之前),否则会报错。

Cookie vs Session 对比

Cookie Session
存放位置 浏览器(客户端) 服务器
安全性 低(用户能看到) 高(用户看不到)
存储量 小(≤4KB) 大(取决于服务器配置)
生命周期 可设置过期时间 浏览器关闭即失效(默认)

一个完整的小例子:用户登录流程

<?php
session_start();

// 模拟登录验证
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST["username"];
$password = $_POST["password"];

// 假设正确的账号密码
if ($username === "admin" && $password === "123456") {
    $_SESSION["username"] = $username;
    $_SESSION["login_time"] = date("Y-m-d H:i:s");
    echo "登录成功!<br>";
    echo '<a href="profile.php">查看个人资料</a>';
    exit;
} else {
    echo "账号或密码错误";
}
}
?>

<form method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<button type="submit">登录</button>
</form>

刷新页面后访问 $_SESSION["username"],数据还在——这就是 Session 的威力。


🔥 实战 35 分钟:3 个递进的小项目

项目 1:访问次数计数器(5 分钟)

记录用户访问网页的次数,刷新页面次数会增加。

<?php
session_start();

// 初始化计数器
if (!isset($_SESSION["visit_count"])) {
$_SESSION["visit_count"] = 0;
}

// 每次访问 +1
$_SESSION["visit_count"]++;

echo "你是第 " . $_SESSION["visit_count"] . " 次访问这个页面";
?>

预期输出

你是第 1 次访问这个页面
(刷新后)你是第 2 次访问这个页面
(再刷新)你是第 3 次访问这个页面

解释:用 $_SESSION["visit_count"] 记录次数,每次刷新 +1


项目 2:记住用户偏好的主题切换(15 分钟)

用户选择「深色/浅色」主题,刷新后记住选择。

<?php
session_start();

// 处理主题切换请求
if (isset($_GET["theme"])) {
$_SESSION["theme"] = $_GET["theme"];
}

// 获取当前主题,默认浅色
$current_theme = $_SESSION["theme"] ?? "light";

// 定义主题样式
$bg_color = ($current_theme === "dark") ? "#333" : "#fff";
$text_color = ($current_theme === "dark") ? "#fff" : "#333";
?>

<!DOCTYPE html>
<html>
<head>
<title>主题切换</title>
</head>
<body style="background:<?php echo $bg_color; ?>; color:<?php echo $text_color; ?>;">
<h1>当前主题:<?php echo $current_theme; ?></h1>

<p>选择主题:</p>
<a href="?theme=light">浅色</a> | 
<a href="?theme=dark">深色</a>

<p>刷新页面,主题会保持不变!</p>

<p>Session ID: <?php echo session_id(); ?></p>
</body>
</html>

预期输出
- 点击「深色」后,页面背景变深色、文字变白色
- 刷新页面,主题不变
- Session ID 每次刷新都一样(说明是同一个会话)

解释:把用户选择存进 $_SESSION["theme"],读取时优先取 session,没有再用默认值。


项目 3:简单的待办清单(持久化版)(15 分钟)

添加待办事项,刷新页面后数据不丢失。

<?php
session_start();

// 初始化待办列表
if (!isset($_SESSION["todos"])) {
$_SESSION["todos"] = [];
}

// 处理添加
if (isset($_POST["todo"]) && !empty($_POST["todo"])) {
$_SESSION["todos"][] = [
    "content" => $_POST["todo"],
    "time" => date("H:i:s")
];
}

// 处理删除
if (isset($_GET["delete"])) {
$index = (int)$_GET["delete"];
if (isset($_SESSION["todos"][$index])) {
    array_splice($_SESSION["todos"], $index, 1);
}
}
?>

<!DOCTYPE html>
<html>
<head>
<title>我的待办</title>
<style>
    body { font-family: Arial; max-width: 500px; margin: 50px auto; }
    .todo-item { padding: 10px; border-bottom: 1px solid #ccc; }
    .todo-item span { color: #888; font-size: 12px; }
    .delete { color: red; text-decoration: none; }
</style>
</head>
<body>
<h1>📝 我的待办清单</h1>

<form method="post">
    <input type="text" name="todo" placeholder="输入待办事项" style="width: 300px;">
    <button type="submit">添加</button>
</form>

<h3>待办列表(共 <?php echo count($_SESSION["todos"]); ?> 项):</h3>

<?php if (empty($_SESSION["todos"])): ?>
    <p>暂无待办,添加一个吧!</p>
<?php else: ?>
    <?php foreach ($_SESSION["todos"] as $index => $todo): ?>
        <div class="todo-item">
            <?php echo ($index + 1) . ". " . htmlspecialchars($todo["content"]); ?>
            <span>(添加于 <?php echo $todo["time"]; ?>)</span>
            <a href="?delete=<?php echo $index; ?>" class="delete" 
               onclick="return confirm('确定删除?')">删除</a>
        </div>
    <?php endforeach; ?>
<?php endif; ?>
</body>
</html>

预期输出
- 添加「买牛奶」,列表显示「1. 买牛奶」
- 刷新页面,列表还有「买牛奶」
- 关闭浏览器重开,数据还在
- 点「删除」后该项消失

解释:用 $_SESSION["todos"] 数组存储所有待办,htmlspecialchars() 防止 XSS 攻击。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:session_start() 位置错误

<?php
// ❌ 错误:在 echo 之后才启动 session
echo "先输出一句话";
session_start(); // 会报错:headers already sent
?>
<?php
// ✅ 正确:session_start() 必须放第一行
session_start();
echo "现在可以输出了";
?>

坑 2:Cookie 过期时间设置错误

<?php
// ❌ 错误:过期时间戳已经过了(过去的时间)
setcookie("token", "abc", time() - 3600); // 删除 cookie 应该这样写
?>
<?php
// ✅ 正确:设置1小时后过期
setcookie("token", "abc", time() + 3600);

// ✅ 正确:明确删除 Cookie(设置过期时间为过去)
setcookie("token", "", time() - 3600);
?>

坑 3:Session 乱码或数据丢失

<?php
// ❌ 错误:文件开头有 BOM 头或空格
​session_start(); // 文件开头有隐藏字符
...
?>
<?php
// ✅ 正确:确保文件是纯 UTF-8 无 BOM
// 检查方法:用十六进制编辑器看文件开头是不是 EF BB BF
session_start();
?>

坑 4:Session 存储路径配置错误

<?php
// ❌ 错误:session_save_path() 也要在 session_start() 之前
session_start();
session_save_path("/some/path"); // 太晚了
?>
<?php
// ✅ 正确:在 session_start() 之前配置
session_save_path("/tmp/sessions");
session_start();
?>

坑 5:生产环境 Session 泄露

<?php
// ❌ 错误:生产环境用默认的 Files 存储
// 多个 PHP-FPM 进程可能互相覆盖
session_start();
$_SESSION["user"] = "sensitive_data";
?>
<?php
// ✅ 正确:生产环境用 Redis 等外部存储
// 或配置 session.cookie_httponly 防止 JS 读取
session_set_cookie_params([
'lifetime' => 3600,
'httponly' => true,  // 禁止 JS 读取 Cookie
'secure' => true     // 只在 HTTPS 传输
]);
session_start();
?>

性能小贴士:Session 懒加载

如果页面不需要 Session 数据,不要每次都启动:

<?php
// 只在需要时才启动 Session
if (need_session()) {
session_start();
// ... 用 session 的代码
}
?>

调试技巧:用 print_r 看 Session

<?php
session_start();

// 任何时候都可以打印看 Session 状态
echo "<pre>";
print_r($_SESSION);
echo "</pre>";
?>

输出类似:

Array
(
[username] => xiaoming
[login_time] => 2024-01-15 10:30:00
[visit_count] => 5
)

✏️ 练习题

练习 1(2 分钟):修改过期时间

  • 输入:把项目 1 的计数器改成「只保留 10 秒」
  • 预期输出:10 秒后再刷新,计数重置为 1
  • 提示:time() + 10

练习 2(2 分钟):添加登录检查

  • 输入:在项目 2 主题切换里加一个 if 判断,如果没登录就不显示切换链接
  • 预期输出:未设置 $_SESSION["username"] 时不显示主题切换
  • 提示:先 $_SESSION["username"] = "test" 模拟登录

练习 3(3 分钟):统计在线人数

  • 输入:用 Session 统计当前有多少人访问过页面(用文件存储访问记录)
  • 预期输出:显示「当前在线人数:X」
  • 提示:用 file_put_contents 把 session_id 写到文件

练习 4(3 分钟):合并待办和主题

  • 输入:把项目 2 和项目 3 的功能合并到一个页面
  • 预期输出:既能切换主题,待办清单也能持久化
  • 提示:两个项目的 session_start() 可以共用

练习 5(5 分钟):分析报错

  • 输入:用户报「Warning: session_start(): Cannot send session cache limiter」
  • 预期输出:说出原因并修复
  • 提示:检查 session_start() 前面有没有输出

作业:做一个「访问日志记录器」

需求描述:记录每个访客的访问时间、访问次数、最后访问时间,把数据持久化存储。

功能点
1. 首次访问显示「欢迎新用户」,记录访问时间
2. 再次访问显示「欢迎回来,你来过 X 次,上次是 Y」
3. 刷新后访问次数 +1

加分项
1. 把日志写入文件(JSON 格式),重启 PHP 也不丢数据
2. 显示「历史上共有 Z 个访客」

验收标准
- 能跑起来
- 刷新页面次数递增
- 关闭浏览器重开,次数继续累加(不清空)

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

本文学到的 3 个核心点
1. Cookie 是钥匙(存客户端),Session 是柜子(存服务器端)
2. setcookie() 设置 Cookie,$_COOKIE 读取;session_start() 启动 Session,$_SESSION 读写
3. Session 依赖 Cookie 传递 session_id,真正数据存在服务器

延伸学习资源
- PHP 官方文档:Session
- 《PHP 核心技术与最佳实践》:第 5 章 Session 深入理解

互动钩子

「你在登录注册时遇到过『记住我』功能失效的问题吗?是 Cookie 过期还是 Session 没启动?评论区聊聊,老粉优先回复!」


下章剧透

「用户填完表单、记住了登录状态,下一步要上传头像怎么办?下一节教你用 PHP 处理文件上传——让用户把图片『快递』到服务器上!」

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。