PHP Cookie
setcookie parameters, reading, deletion, and why never to store secrets.
Setting a cookie
setcookie() must run before any output since it sends a header.
<?php
// 会话 Cookie(关浏览器就失效)
setcookie("user", "Tom");
// 带过期时间(1 小时后)
setcookie("theme", "dark", [
"expires" => time() + 3600,
"path" => "/",
"httponly" => true,
"samesite" => "Lax",
]);
echo "Cookie 已发送";Cookie 已发送The response carries Set-Cookie: user=Tom and Set-Cookie: theme=dark; .... The one without an expiry is a session cookie, cleared when the browser closes.
| Option | Meaning | Recommended |
|---|---|---|
| expires | 过期时间戳,0 表示会话 Cookie | 按需 |
| path | 生效路径 | / |
| domain | 生效域名 | 不填,仅当前域 |
| secure | 只走 HTTPS | 线上必开 |
| httponly | JS 读不到,防 XSS 窃取 | 必开 |
| samesite | 跨站请求是否携带 | Lax 或 Strict |
Reading cookies
Cookies return in the Cookie header and land in $_COOKIE. A value set this request is not visible until the next one.
<?php
// 前提:上一个响应已写入 user=Tom,本次请求带回来了
echo $_COOKIE["user"] ?? "未设置", "\n";
echo $_COOKIE["theme"] ?? "light", "\n";
// 遍历全部
foreach ($_COOKIE as $k => $v) {
echo "$k=$v\n";
}Tom
dark
user=Tom
theme=dark$_COOKIE values are user-editable. Storing is_admin=1 in a cookie is self-deception — edit the value, become admin.Deleting a cookie
Delete by expiring it in the past. Use the same path and domain, or the browser treats it as a different cookie.
<?php
// 删除:过期时间设为过去
setcookie("user", "", [
"expires" => time() - 3600,
"path" => "/",
]);
// 同时清掉 $_COOKIE 里的副本,避免本次请求还读到旧值
unset($_COOKIE["user"]);
echo isset($_COOKIE["user"]) ? "还在" : "已删除", "\n";已删除What to store
| Data | Cookie | Session |
|---|---|---|
| 主题、语言偏好 | 可以 | 也可以 |
| 「记住我」的 token | 可以(随机串 + 服务端校验) | — |
| 用户 ID、角色 | 不行 | 可以 |
| 购物车内容 | 小可以,大会超 4KB | 推荐 |
| 密码、密钥 | 绝对不行 | 不行(会话 ID 泄露等于全丢) |
A cookie caps around 4KB, about 50 per domain. A full cart belongs in the session or database.
Signed cookies
To keep data client-side without tampering, add an HMAC signature — the approach behind Laravel and Django encrypted cookies.
<?php
const SECRET = "change-me-in-production";
function sign(string $value): string {
$mac = hash_hmac("sha256", $value, SECRET);
return base64_encode($value) . "." . $mac;
}
function verify(string $signed): ?string {
$parts = explode(".", $signed, 2);
if (count($parts) !== 2) {
return null;
}
[$b64, $mac] = $parts;
$value = base64_decode($b64, true);
if ($value === false) {
return null;
}
if (!hash_equals(hash_hmac("sha256", $value, SECRET), $mac)) {
return null;
}
return $value;
}
$token = sign("user=42&role=member");
echo verify($token), "\n";
// 篡改后校验失败
$bad = str_replace("dXNlcj00", "dXNlcj05", $token);
var_dump(verify($bad));user=42&role=member
NULLhash_equals(), never ==. Short-circuit comparison leaks timing information, allowing byte-by-byte forgery.设置 Cookie
setcookie() 必须在任何输出之前调用,因为它要发 HTTP 头。
<?php
// 会话 Cookie(关浏览器就失效)
setcookie("user", "Tom");
// 带过期时间(1 小时后)
setcookie("theme", "dark", [
"expires" => time() + 3600,
"path" => "/",
"httponly" => true,
"samesite" => "Lax",
]);
echo "Cookie 已发送";Cookie 已发送实际发出的响应头是 Set-Cookie: user=Tom 和 Set-Cookie: theme=dark; ...。响应头里不含过期时间的那个是会话 Cookie,浏览器关闭即清。
| 参数 | 含义 | 建议 |
|---|---|---|
| expires | 过期时间戳,0 表示会话 Cookie | 按需 |
| path | 生效路径 | / |
| domain | 生效域名 | 不填,仅当前域 |
| secure | 只走 HTTPS | 线上必开 |
| httponly | JS 读不到,防 XSS 窃取 | 必开 |
| samesite | 跨站请求是否携带 | Lax 或 Strict |
读取 Cookie
Cookie 随请求的 Cookie 头回到服务器,PHP 填进 $_COOKIE。**本次 setcookie 设的值在当次请求里读不到**,要等下一次请求。
<?php
// 前提:上一个响应已写入 user=Tom,本次请求带回来了
echo $_COOKIE["user"] ?? "未设置", "\n";
echo $_COOKIE["theme"] ?? "light", "\n";
// 遍历全部
foreach ($_COOKIE as $k => $v) {
echo "$k=$v\n";
}Tom
dark
user=Tom
theme=dark$_COOKIE 里的值同样是用户可篡改的。把 is_admin=1 存进 Cookie 是最经典的自欺欺人——改个值就成管理员了。删除 Cookie
删除就是把过期时间设成过去。必须带上相同的 path 和 domain,否则删不掉——浏览器会认为是另一个 Cookie。
<?php
// 删除:过期时间设为过去
setcookie("user", "", [
"expires" => time() - 3600,
"path" => "/",
]);
// 同时清掉 $_COOKIE 里的副本,避免本次请求还读到旧值
unset($_COOKIE["user"]);
echo isset($_COOKIE["user"]) ? "还在" : "已删除", "\n";已删除Cookie 存什么
| 数据 | Cookie | Session |
|---|---|---|
| 主题、语言偏好 | 可以 | 也可以 |
| 「记住我」的 token | 可以(随机串 + 服务端校验) | — |
| 用户 ID、角色 | 不行 | 可以 |
| 购物车内容 | 小可以,大会超 4KB | 推荐 |
| 密码、密钥 | 绝对不行 | 不行(会话 ID 泄露等于全丢) |
单个 Cookie 上限约 4KB,单个域名约 50 个。购物车塞满商品就该挪到会话或数据库。
签名 Cookie
需要客户端保存又不想被篡改,就加 HMAC 签名。Laravel、Django 的加密 Cookie 都是这个思路。
<?php
const SECRET = "change-me-in-production";
function sign(string $value): string {
$mac = hash_hmac("sha256", $value, SECRET);
return base64_encode($value) . "." . $mac;
}
function verify(string $signed): ?string {
$parts = explode(".", $signed, 2);
if (count($parts) !== 2) {
return null;
}
[$b64, $mac] = $parts;
$value = base64_decode($b64, true);
if ($value === false) {
return null;
}
if (!hash_equals(hash_hmac("sha256", $value, SECRET), $mac)) {
return null;
}
return $value;
}
$token = sign("user=42&role=member");
echo verify($token), "\n";
// 篡改后校验失败
$bad = str_replace("dXNlcj00", "dXNlcj05", $token);
var_dump(verify($bad));user=42&role=member
NULLhash_equals(),不能用 ==。== 是短路比较,攻击者能通过响应时间逐字节猜出正确签名——这叫时序攻击。