 函数)
前言setcookie()的作用是让 PHP 在响应里追加一个Set-Cookie响应头从而把一小段数据交给浏览器保存下来并在后续请求里自动带回来。听起来很简单但它牵扯到 HTTP 协议本身的一条硬性限制Cookie 属于响应头的一部分必须在任何响应体输出之前发送。围绕这个函数最常见的三个误解是一以为调用setcookie()后立刻就能从$_COOKIE里读出来实际上是下一次请求才有二以为设了httponly就万无一失它只能挡住脚本读取挡不住篡改三以为 Cookie 有保密性它明文存在用户机器上用户想怎么改就怎么改。本文按 PHP 官方手册讲清两种调用签名、各参数尤其是samesite的含义、删除 Cookie 的正确姿势以及把 Cookie 用在不该用的地方会带来什么后果。一、两种签名与参数含义第一种是传统的位置参数写法setcookie(string $name, string $value , int $expires_or_options 0,string $path , string $domain , bool $secure false,bool $httponly false): bool第二种自 PHP 7.3.0 起提供把后面几个参数合并成关联数组setcookie(string $name, string $value , array $options []): bool手册特别注明数组形式的签名不支持命名参数。$options允许的键有expires、path、domain、secure、httponly、samesite、partitionedpartitioned是更晚的版本才加入的。没给出的键会沿用与位置参数相同的默认值samesite如果省略PHP 不会发送任何 SameSite 属性。参数类型说明namestringCookie 名。取回时用$_COOKIE[name]valuestringCookie 值。会明文存在用户机器上不要放敏感信息expires_or_optionsintUnix 时间戳不是日期字符串。传0或省略表示「会话 Cookie」浏览器关闭即失效pathstring生效路径。设为/表示整站可见设为/admin/表示只在/admin/及其子目录可见domainstring生效域名。设为example.com则其所有子域都可见securebool只在 HTTPS 连接上发送httponlybool不允许 JavaScript 读取document.cookie拿不到能降低 XSS 场景下的会话窃取风险samesitestring取值只应是None、Lax或StrictNone必须同时开启secure否则会被浏览器丢弃expires_or_options用时间戳而不是Wdy, DD-Mon-YYYY HH:MM:SS GMT这种日期格式是因为时区转换由 PHP 内部完成。常见写法是time() 60 * 60 * 24 * 3030 天后过期。二、必须在任何输出之前调用setcookie()干的事本质上是加一个响应头。按 HTTP 协议响应头必须先于响应体发出一旦 PHP 已经向浏览器输出了任何字节头部就锁死了。手册的措辞是如果有输出先于本函数存在setcookie()会失败并返回false。这里的「输出」比想象中宽得多包括任何echo、print、printf?关闭标签之后的空行或空格这就是纯 PHP 文件不要写结尾?的原因被include进来的文件产生的输出文件开头被编辑器加上的 UTF-8 BOM\xEF\xBB\xBFPHP 启动阶段的错误或警告信息。配置里开了output_buffering时输出会先进入缓冲区而不是直接发出看起来「晚点调用也没事」但这只是把问题掩盖了缓冲区一旦刷出同样的错误会出现在另一个位置更难排查。定位这类问题最有效的手段是headers_sent()?php // 适用于 PHP 8.0if (headers_sent($file, $line)) {// $file 与 $line 指向「第一次输出」发生的位置error_log(响应头已发送位置: {$file}:{$line});} else {setcookie(theme, dark, [expires time() 86400,path /,secure true,httponly true,samesite Lax,]);}三、读取、删除与安全属性读取只能在下一次请求里发生。$_COOKIE是 PHP 在处理请求时根据请求头里的Cookie字段一次性填充的当前请求中新设置的 Cookie 不会自动出现在里面。本次请求内要继续用这个值就在变量里留一份。删除 Cookie 的做法是把过期时间设到过去并且path与domain必须和当初设置时一致否则浏览器会认为这是另一个 Cookie旧的依然保留?php // 适用于 PHP 8.0setcookie(theme, , [expires time() - 3600,path /,secure true,httponly true,samesite Lax,]);关于安全属性要准确理解它们能防什么httponly让 JavaScript 读不到这个 Cookie能减少 XSS 造成的会话窃取面但对手可以直接在浏览器里改 Cookiesecure保证只在加密连接上传输防的是明文链路上的嗅探samesite控制跨站请求是否带上 Cookie是缓解 CSRF 的基础手段之一。绝对不要把有业务含义的数据直接放在 Cookie 里。「用户 ID」「角色是 admin」「商品价格」这类字段用户改一下 Cookie 就改了业务逻辑。正确做法是只放一个不可猜的会话标识真实状态放在服务端如果确实需要在客户端保存状态就要加签名?php // 适用于 PHP 8.0declare(strict_types1);function signValue(string $value, string $key): string{$payload base64_encode($value); // 编码不是加密$mac hash_hmac(sha256, $payload, $key);return $payload . . . $mac;}function verifyValue(string $cookie, string $key): string|false{$parts explode(., $cookie, 2);if (count($parts) ! 2) {return false;}[$payload, $mac] $parts;$calc hash_hmac(sha256, $payload, $key);if (!hash_equals($mac, $calc)) {return false;}$decoded base64_decode($payload, true);return $decoded false ? false : $decoded;}$key server-side-secret-key; // 真实项目里来自配置不要硬编码在仓库中$value signValue(themedark, $key);setcookie(pref, $value, [expires time() 86400,path /,secure true,httponly true,samesite Lax,]);注意这里用base64_encode()只是为了把任意字节塞进 Cookie 值里不产生非法字符它提供的是编码不是保密性真正防篡改的是hash_hmac()加hash_equals()的常量时间比较。常见坑点❌ 设完 Cookie 立刻读$_COOKIE[theme]—— 当前请求里它还不存在只有浏览器的下一次请求才会把它带上来。✅ 本次请求内用变量保存要用的值$_COOKIE只用来读「上一次带来的」数据。❌ 在setcookie()前面有echo、有?之后的空行、或文件带 UTF-8 BOM导致Warning: Cannot modify header information - headers already sent。✅ 纯 PHP 文件不写结尾?编辑器保存为「无 BOM 的 UTF-8」并把业务逻辑放在输出之前用headers_sent($file, $line)打印出「第一次输出」的位置来定位。❌ 删除 Cookie 时只写setcookie(theme, )—— 没有过期时间浏览器把它当成一个「值变为空」的会话 Cookie 继续保存并发送。✅ 传过去的expires例如time() - 3600且path、domain与设置时完全一致。❌ 用 Cookie 保存用户 ID、角色、余额等有业务含义的字段 —— 全部可被用户随意修改。✅ 只存不透明的会话标识状态放服务端必须放客户端时用 HMAC 签名并在服务端校验。❌ 不设置samesite—— 省略时 PHP 不会输出这个属性跨站请求会带上 CookieCSRF 面变大。✅ 显式写samesite Lax或Strict确实需要None时必须同时secure true否则浏览器会直接拒绝这个 Cookie。❌ 以为写入的值会原样存进浏览器 —— 值部分会被自动 URL 编码读回来时由 PHP 自动解码赋给$_COOKIE前后看到的形态可能不同。✅ 需要完全不做编码地发送用setrawcookie()但让 PHP 编码通常才是更稳的选择。❌ 把 Cookie 当本地存储塞进大量数据 —— 浏览器对单个 Cookie 大小和单域名 Cookie 总量都有限制且每个请求都会带上直接反映在带宽上。✅ Cookie 里只放会话标识其余数据留在服务端。❌ 用isset($_COOKIE[x])判断「用户是否登出」 ——setcookie(x, )之后$_COOKIE[x]是空字符串isset()依然为真判断方向正好反了。✅array_key_exists(x, $_COOKIE) $_COOKIE[x] ! 并把「是否登录」这个判断放在服务端会话里而不是靠 Cookie 是否存在。总结要点结论两种签名位置参数PHP 7.3 另有$options数组形式不支持命名参数调用时机必须在任何输出之前否则失败并返回false读取时机$_COOKIE只包含本次请求带来的 Cookie新设的要下次请求才读得到删除方法过期时间设为过去且path/domain与设置时一致安全属性secure防明文链路httponly防脚本读取samesite缓解 CSRF存什么只存不透明标识有业务含义的数据必须服务端校验或 HMAC 签名不存什么密码、令牌明文、大量数据、任何可被篡改后直接影响授权的字段一句话总结setcookie()本质是「往响应头里加一行」所以它受制于输出顺序而 Cookie 本身是用户可读可改的客户端存储把「不信任 Cookie 内容」和「关键状态放服务端」这两条守住剩下的都只是属性配置问题。