PHP 会话与 Cookie

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

PHP 8.4

HTTP 是无状态协议:每个请求互相独立。要「记住用户」,就得靠 Cookie(存在浏览器)与 Session(存在服务器)。

两者的分工

Cookie Session
存储位置 浏览器 服务器(默认文件系统)
容量 约 4 KB 受服务器限制,可存较多
安全性 用户可见可改,不可信 用户只拿到 ID,内容不可见
典型用途 记住偏好、非敏感标记 登录态、购物车、权限信息

Session 依赖 Cookie:Session ID 默认通过名为 PHPSESSID 的 Cookie 传递。

使用 Session

<?php
declare(strict_types=1);

session_start();

// 写入
$_SESSION["user_id"] = 42;
$_SESSION["cart"] = ["item_1" => 2];

// 读取
$userId = $_SESSION["user_id"] ?? null;

// 删除单个键
unset($_SESSION["cart"]);

session_start() 必须在任何输出之前调用,否则会报「headers already sent」。常见踩坑是文件开头有 BOM 或 ?> 之后有多余换行。

销毁登录态

退出登录要彻底,三步缺一不可:

session_start();

$_SESSION = [];                                  // 1. 清空数据

if (ini_get("session.use_cookies")) {            // 2. 删除会话 Cookie
    $params = session_get_cookie_params();
    setcookie(
        session_name(),
        "",
        [
            "expires" => time() - 42000,
            "path" => $params["path"],
            "domain" => $params["domain"],
            "secure" => $params["secure"],
            "httponly" => $params["httponly"],
            "samesite" => $params["samesite"],
        ]
    );
}

session_destroy();                               // 3. 销毁服务端会话

只调用 session_destroy() 而不清空 $_SESSION,本次请求里的数据仍可访问。

会话固定攻击与 ID 轮换

登录成功后必须重新生成 Session ID,否则攻击者可以诱导用户使用已知 ID 登录后劫持会话:

session_start();

if (password_verify($password, $hash)) {
    session_regenerate_id(true);   // true = 同时删除旧会话文件
    $_SESSION["user_id"] = $user["id"];
    $_SESSION["logged_in_at"] = time();
}

PHP 7.3+ 支持数组形式的选项(推荐,可读性好):

setcookie("theme", "dark", [
    "expires" => time() + 86400 * 30,   // 30 天后过期
    "path" => "/",
    "domain" => "",                     // 当前域
    "secure" => true,                   // 仅 HTTPS 传输
    "httponly" => true,                 // JS 不可读取
    "samesite" => "Lax",                // 跨站请求限制
]);

各选项的作用:

  • secure:只在 HTTPS 下发送。本地 HTTP 开发时设为 false,上线必须 true。
  • httponly:JS 无法通过 document.cookie 读取,缓解 XSS 窃取。
  • samesite:Lax(推荐默认,允许常规导航带 Cookie)、Strict(最严,跨站一律不带)、None(跨站也带,必须同时 secure)。

删除 Cookie 用过期时间:

setcookie("theme", "", ["expires" => time() - 3600, "path" => "/"]);
$theme = $_COOKIE["theme"] ?? "light";

// 只允许白名单值,防止被用户改成任意内容
$theme = in_array($theme, ["light", "dark"], true) ? $theme : "light";

Cookie 内容用户可随意修改,取出来后必须校验,绝不能当作可信数据源(例如不要把用户 ID 存 Cookie 来判断身份)。

Session 存储与配置

默认存在服务器的临时目录。想改存储位置或方式:

// 改存储目录(脚本内设置,需在 session_start 前)
session_save_path(__DIR__ . "/storage/sessions");
session_start();

常用 php.ini 配置:

session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.gc_maxlifetime = 1440      ; 会话数据存活秒数
session.use_strict_mode = 1        ; 只接受自己创建的会话 ID,防会话固定

session.use_strict_mode = 1 建议始终开启。

一个完整例子:登录与访问控制

<?php
declare(strict_types=1);

require __DIR__ . "/bootstrap.php";   // 假设这里完成了 session 配置

session_start();

function requireLogin(): void
{
    if (empty($_SESSION["user_id"])) {
        header("Location: /login.php", true, 303);
        exit;
    }
}

function login(int $userId): void
{
    session_regenerate_id(true);
    $_SESSION["user_id"] = $userId;
    $_SESSION["logged_in_at"] = time();
}

function logout(): void
{
    $_SESSION = [];
    session_destroy();
}

requireLogin();
echo "当前用户 ID:" . (int) $_SESSION["user_id"];

访问控制放在每个需要保护的入口,而不是只放在登录页:用户可以直接访问任意 URL。

常见错误与注意事项

  1. session_start() 前有输出:报 headers already sent,检查 BOM 与 ?> 后的空行。
  2. 登录后不轮换 Session ID:会话固定攻击风险,用 session_regenerate_id(true)。
  3. 退出时只 session_destroy():$_SESSION 本次请求仍可用,先清空再销毁。
  4. 把敏感数据存 Cookie:用户可见可改,敏感信息只存 Session。
  5. Cookie 值不校验直接用:用户可伪造,用白名单过滤。
  6. 生产环境不开 secure / httponly:明文传输或被 JS 窃取。
  7. 只在登录页做权限检查:每个受保护页面都要独立校验。

小结

  • Cookie 存浏览器、不可信、约 4 KB;Session 存服务器、通过 Session ID 关联。
  • session_start() 必须在任何输出之前。
  • 登录成功 session_regenerate_id(true);退出时清空 $_SESSION + 删 Cookie + session_destroy()。
  • setcookie 上线时给 secure、httponly、samesite。
  • 权限校验放在每个受保护入口,不只在登录流程里。

下一步