PHP Cookie

setcookie parameters, reading, deletion, and why never to store secrets.

Beginner
Key PointsCookies live in the user's browser and are readable and editable — never store secrets.

Setting a cookie

setcookie() must run before any output since it sends a header.

PHP
<?php
// 会话 Cookie(关浏览器就失效)
setcookie("user", "Tom");

// 带过期时间(1 小时后)
setcookie("theme", "dark", [
    "expires"  => time() + 3600,
    "path"     => "/",
    "httponly" => true,
    "samesite" => "Lax",
]);

echo "Cookie 已发送";
Output
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.

OptionMeaningRecommended
expires过期时间戳,0 表示会话 Cookie按需
path生效路径/
domain生效域名不填,仅当前域
secure只走 HTTPS线上必开
httponlyJS 读不到,防 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
<?php
// 前提:上一个响应已写入 user=Tom,本次请求带回来了
echo $_COOKIE["user"] ?? "未设置", "\n";
echo $_COOKIE["theme"] ?? "light", "\n";

// 遍历全部
foreach ($_COOKIE as $k => $v) {
    echo "$k=$v\n";
}
Output
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
<?php
// 删除:过期时间设为过去
setcookie("user", "", [
    "expires" => time() - 3600,
    "path"    => "/",
]);

// 同时清掉 $_COOKIE 里的副本,避免本次请求还读到旧值
unset($_COOKIE["user"]);

echo isset($_COOKIE["user"]) ? "还在" : "已删除", "\n";
Output
已删除

What to store

DataCookieSession
主题、语言偏好可以也可以
「记住我」的 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
<?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));
Output
user=42&role=member
NULL
Compare signatures with hash_equals(), never ==. Short-circuit comparison leaks timing information, allowing byte-by-byte forgery.

Friends & Partners