先说几个核心判断:setcookie()必须在任何输出之前调用,否则直接报错;而刚设置完的Cookie,在同一个请求里用$_COOKIE是读不到的——这不是什么Bug,是机制本身就如此设计。
setcookie() 报“headers already sent”怎么快速定位
问题的本质,其实就一句话:HTTP响应头已经发出去了,这时候再想追加Set-Cookie,服务器自然不答应。但最常见的“罪魁祸首”,往往不是代码逻辑本身,而是一些看不见的“隐形字符”:
- PHP文件开头带着UTF-8 BOM(尤其是Windows下的编辑器,保存时默认就会加上)。用
hexdump -C yourfile.php | head看一眼,如果前三个字节是ef bb bf,那就是它了。 - 文件末尾多了一个换行、空格,或者
?>后面还跟着回车——这些都会被视为输出。 - 引入的配置文件,比如
config.php里,如果存在echo、print或者任何意外输出,也会导致这个问题。 short_open_tag开启的情况下,文件如果以开头,但服务器实际没有启用这个配置,那第一行就会被当作普通文本输出——典型的“我以为没问题,实际已经出事了”。
临时调试的话,可以用ob_start()把输出缓存起来,等脚本结束再统一发送。但得注意,这只是一个临时手段,线上环境这么做会增加内存压力,不推荐。
为什么刚 setcookie() 就 echo $_COOKIE['xxx'] 是空的
很多人第一次碰到这个坑的时候,都会怀疑自己是不是写错了。其实原理很简单:$_COOKIE是PHP在请求一开始,从浏览器发来的HTTP请求头里一次性解析出来的数组。它只反映“上一次响应后,浏览器带回来的Cookie”。而setcookie()干的事情,是“告诉浏览器,下次请求带上这个”,它压根不会修改当前请求里的$_COOKIE。
- 想验证是否设置成功?必须刷新页面,触发一个新请求,再读
$_COOKIE。 - 调试时如果非要手动同步,可以写一句
$_COOKIE['user_id'] = '123';——但这只是模拟,不影响客户端真实状态。 - 还有一点,永远不要信任
$_COOKIE的原始值。它没有签名、没有加密,用户可以随意篡改。涉及权限判断,必须查数据库或者验HMAC签名。
path 和 domain 设错会导致 Cookie“设了却读不到”
Cookie的可见性由path和domain共同决定。设错了,就等于把钥匙放在了隔壁房间,自己当然找不到。
path默认是当前脚本所在的路径。比如在/admin/login.php里调用setcookie('token', 'abc'),默认path='/admin/'。这时候,/api/下的脚本就收不到这个Cookie。- 全站通用,必须显式写成
path='/'。如果只想限定某个子目录,比如购物车,就写path='/cart/'。 domain默认是当前主机名,比如example.com。如果想让www.example.com和api.example.com共享Cookie,那就得写成domain='.example.com'——注意开头的那个点,这个点不能省。- 删除Cookie的时候,
path和domain必须和设置时完全一致,否则旧的Cookie会一直残留,删不掉。
安全参数 secure、httponly、samesite 不是可选项
生产环境里,如果不加这些参数,那基本等于把钥匙挂在门把手上,谁都能拿走。
secure => true:强制Cookie只走HTTPS。HTTP页面不会发送它,但注意,如果不是HTTPS环境,设了也无效。httponly => true:JS没办法通过document.cookie读取,能大幅降低XSS泄露的风险。samesite => 'Lax'或'Strict':防御CSRF。PHP 7.3以上版本原生支持用数组参数传入,老版本就只能用header()手动构造了。- 过期时间必须是整数时间戳。
time() + 86400是对的,但写成'2025-01-01'就错了——会被当成0,立刻过期。
这里有一个容易被忽略的细节:samesite在旧版PHP里,没法通过setcookie()的数组参数设置,必须切换到header('Set-Cookie: ... samesite=Lax; ...')。而且日期必须用GMT格式(gmdate()),否则浏览器直接拒收。这个坑,踩过的人都知道。