PHP8.0怎么实现Session和Cookie管理
前言先把一个容易误解的前提说清楚Session 与 Cookie 不是 PHP 8.0 才有的能力。Session 机制早在 PHP 4 时代就已经是内置功能setcookie()更是从 PHP 3 就存在。标题里的 8.0 应当理解为在 PHP 8.0 环境下怎么写而不是8.0 引入的新特性。与本文直接相关的、确实属于较新版本的改动是setcookie()与session_set_cookie_params()自 PHP 7.3 起支持数组形式的选项从此设置SameSite这类参数不需要再去拼path、domain这些位置参数而 PHP 8.0 带来的命名参数、严格类型等语法让这些调用的可读性明显变好。症状层面也有典型场景登录后刷新页面就掉线登录成功但把 Session ID 直接放在 URL 里被爬虫抓到跨站请求时 Cookie 莫名不发送session_start()报 headers already sent把数据库连接对象塞进$_SESSION后整个会话读取失败。本文按原理 → Cookie → Session → 完整可运行示例 → 坑点的顺序给出一套在 PHP 8.0 环境里可靠、安全的 Session 与 Cookie 管理办法。一、原理一个在服务端一个在客户端两者的分工非常清晰Cookie是存在客户端的小段键值数据。服务端通过响应头Set-Cookie下发浏览器在之后的同域请求里通过请求头Cookie带回来PHP 把它解析进$_COOKIE超全局数组。Session是存在服务端的数据默认存成文件客户端只持有一个标识符。这个标识符通常经由 Cookie 传递Cookie 名由session.name决定默认是PHPSESSID。由此推导出三条铁律Cookie 的内容不可信。用户随时可以改自己浏览器里的 Cookie所以权限、金额、用户 ID 这类信息只能放服务端。下发 Cookie 就是发响应头。setcookie()、session_start()都必须在任何输出之前调用否则报 headers already sent。Session 的凭证等价于密码。拿到 Session ID 就等于拿到登录态所以它必须是不可预测的、并且要防范会话固定攻击session fixation。常用属性的取值建议属性作用建议expires绝对过期时间戳会话级 Cookie 可不设让浏览器关闭即失效path生效路径通常设为/domain生效域名留空表示当前域跨子域共享才显式设置secure仅经 HTTPS 传输生产环境一律开启httponly禁止 JavaScript 读取一律开启能挡掉大量 XSS 窃取samesite是否随跨站请求发送一般Lax纯跨站场景才用None二、Cookie用数组选项写别再拼位置参数PHP 7.3 起setcookie()的第三个参数既可以是旧的路径字符串也可以是一个选项数组。数组形式的好处是每个参数都叫得出名字尤其是samesite在旧的位置参数里根本表达不了。?php declare(strict_types1); // 写 Cookie有效期 30 天 setcookie(theme, dark, [ expires time() 60 * 60 * 24 * 30, path /, secure true, // 需要 HTTPS本地调试可临时置 false httponly true, samesite Lax, ]); // 读 Cookie始终当作不可信输入处理 $theme $_COOKIE[theme] ?? light; if (!in_array($theme, [light, dark], true)) { $theme light; } // 删除 Cookie必须用与写入时相同的 path / domain并让过期时间成为过去 setcookie(theme, , [ expires time() - 3600, path /, httponly true, ]);两个容易忽略的细节setcookie()会对值做 URL 编码读取时 PHP 自动解码所以不要自己再编码一次不希望编码时用setrawcookie()删除时必须复用写入时的path与domain否则浏览器会认为这是两个不同的 Cookie旧的删不掉。三、Session开启时的选项决定安全性session_start()接受一个选项数组这些选项会覆盖php.ini里的同名配置。把关键项写在代码里比依赖服务器配置更可靠——尤其是换机器部署的时候。?php declare(strict_types1); session_start([ use_strict_mode 1, // 拒绝客户端伪造的、服务端没发过的会话 ID cookie_httponly 1, cookie_secure 1, // 生产环境开启 cookie_samesite Lax, gc_maxlifetime 1800, // 服务端会话数据 30 分钟过期 ]); // 写入会话 $_SESSION[uid] 42; $_SESSION[name] alice; // 登录成功那一刻换一个新的会话 ID防止会话固定攻击 session_regenerate_id(true); // 参数 true 表示同时删除旧会话文件 // 登出清数据、毁会话、删 Cookie三步都做 $_SESSION []; if (ini_get(session.use_cookies)) { $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] ?? Lax, ]); } session_destroy();其中use_strict_mode值得单独强调它为 1 时PHP 会拒绝任何服务端没有生成过的会话 ID从根源上堵住会话固定攻击。为 0 时不少旧配置的默认值就是 0攻击者可以先构造一个已知的会话 ID 诱导受害者使用再拿着同一个 ID 冒充受害者。session_regenerate_id(true)的调用时机是权限发生变化时登录成功、退出登录、提权。日常页面请求不需要反复调用。四、完整可运行示例三个脚本 命令行验证下面三个脚本需要PHP 8.0 或更高放在同一个目录用php -S 127.0.0.1:8000启动内置服务器即可运行。为了让示例聚焦在会话与 Cookie 本身输出全部是纯文本。login.php?php declare(strict_types1); session_start([ use_strict_mode 1, cookie_httponly 1, cookie_samesite Lax, ]); // 演示用的账号表真实项目应查数据库 $users [ alice password_hash(secret, PASSWORD_DEFAULT), ]; $user $_GET[user] ?? ; $pass $_GET[pass] ?? ; if ($user || $pass ) { http_response_code(400); echo 缺少参数: 请带 user 与 pass 访问\n; exit; } if (!isset($users[$user]) || !password_verify($pass, $users[$user])) { http_response_code(401); echo 账号或密码错误\n; exit; } // 关键一步登录成功即更换会话 ID session_regenerate_id(true); $_SESSION[uid] 1001; $_SESSION[name] $user; // 顺带写一个非敏感偏好到 Cookie 里演示数组选项写法 setcookie(last_login, (string) time(), [ expires time() 86400, path /, httponly true, samesite Lax, ]); echo 登录成功: {$user}\n; echo 会话 ID: , session_id(), \n;me.php?php declare(strict_types1); session_start([use_strict_mode 1]); if (empty($_SESSION[uid])) { http_response_code(401); echo 未登录\n; exit; } echo 当前用户: , $_SESSION[name], \n; echo 上次登录时间戳: , $_COOKIE[last_login] ?? (无), \n;logout.php?php declare(strict_types1); session_start([use_strict_mode 1]); $_SESSION []; if (ini_get(session.use_cookies)) { setcookie(session_name(), , [ expires time() - 42000, path /, ]); } session_destroy(); echo 已登出\n;启动并验证php -S 127.0.0.1:8000 # 另开一个终端 # 1. 登录同时把 Set-Cookie 头打印出来并把 Cookie 存到文件 curl -i -c jar.txt 127.0.0.1:8000/login.php?useralicepasssecret # 2. 带着 Cookie 请求应当看到当前用户: alice curl -b jar.txt 127.0.0.1:8000/me.php # 3. 登出 curl -b jar.txt 127.0.0.1:8000/logout.php # 4. 再次访问应当返回 401 curl -i -b jar.txt 127.0.0.1:8000/me.php第 1 步的响应头里能看到Set-Cookie: PHPSESSID...; path/; HttpOnly; SameSiteLax登出后的第 4 步会返回 401说明整条链路都通。五、多机部署时的会话存储默认的会话处理器把数据存成文件多台应用服务器之间无法共享——用户在第 1 台登录被负载均衡打到第 2 台就掉线。解决办法是换集中式存储在php.ini里配置; 使用 Redis 作为会话存储多台机器共享同一份会话数据 session.save_handler redis session.save_path tcp://127.0.0.1:6379?timeout1 session.use_strict_mode 1换成 Redis 之后session_regenerate_id()、session_destroy()这些调用方式完全不变只是数据落到了共享存储上。常见坑点❌session_start()之前有输出空行、空格、UTF-8 BOM✅ 把它放在脚本第一行且文件存为无 BOM 的 UTF-8任何字符一旦被发送响应头就锁定了会话 Cookie 下发不出去表现为登录没反应日志里是那句经典的 headers already sent。❌ 登录成功后不调用session_regenerate_id()✅ 登录成功立即调用session_regenerate_id(true)不换 ID 的话攻击者事先拿到的会话 ID 在用户登录后就变成了有效的登录态凭证。❌ 把访问令牌、用户余额、权限位放进 Cookie ✅ 放服务端 Session 或服务端校验Cookie 是客户端可随意编辑的读取时必须当成不可信输入。❌ 登出只写$_SESSION []✅ 还要session_destroy()并让会话 Cookie 立即过期只清数组不销毁会话服务端文件还在会话 ID 仍然能被复用。❌ 用setcookie($name, , [expires 0])删除 Cookie ✅ 用time() - 3600这样的过去时间过期为 0 的语义会因浏览器而异用明确的过去时间才稳妥同时要复用写入时的path与domain。❌SameSite设为None却没开Secure✅ 两者必须同时满足现代浏览器会直接拒绝这种组合表现为跨站请求始终收不到 Cookie而单站点测试完全正常。❌ 往$_SESSION里塞PDO连接、闭包等不可序列化的东西 ✅ 只存标量或可序列化的简单对象会话写入时序列化失败读取时整个会话数据都拿不回来报出的错误信息往往和序列化离得很远。❌ 用md5(uniqid())自己造会话 ID ✅ 交给session_create_id(null)或直接使用 PHP 生成的 ID自造的 ID 可预测性不可控而会话 ID 一旦被猜中攻击者不需要密码就能进入账号。总结需求API关键参数写 Cookiesetcookie()数组选项里的expires、httponly、samesite读 Cookie$_COOKIE一律按不可信输入校验删 Cookiesetcookie()过期时间设为过去路径与域名保持一致开启会话session_start()use_strict_mode、cookie_httponly、cookie_samesite登录后换 IDsession_regenerate_id()传true同时删除旧会话文件登出session_destroy()配合清空数组与删除 Cookie多机共享会话存储处理器指向 Redis 等集中式存储Session 和 Cookie 都不是 PHP 8.0 的新功能8.0 带来的变化主要在语法层面以及自 7.3 起就可用的数组形式选项让SameSite、HttpOnly这些安全属性终于能在一个调用里写清楚。真正决定安全性的仍然是那几条老规矩Cookie 里的内容永远不可信、登录成功必须更换会话 ID、登出要清数据与凭证、跨站场景下SameSiteNone必须搭配Secure。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →