在页面开头调用 session_start(),然后像操作普通数组一样读写 $_SESSION['key']:PHP 把它保存在服务器上,并在访问者下一次请求时再找回来。对于需要由浏览器保存的小值,调用 setcookie('name', 'value', time() + 86400),之后的请求中从 $_COOKIE['name'] 读回。
<?php
session_start(); // before any output
setcookie('theme', 'dark', time() + 60 * 60 * 24 * 30); // 30 days, also before output
$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
$theme = $_COOKIE['theme'] ?? 'light'; // set on an earlier visit
echo "You have opened this page {$_SESSION['visits']} times.";
session 和 cookie 只有在浏览器发出多个 HTTP 请求时才有意义,所以需要它们的示例以普通代码的形式展示。围绕它们的逻辑只是普通的 PHP 数组操作,这部分可以在这里运行:在每个可运行的代码块中,$session 代替 $_SESSION,每次调用代表一次请求。
PHP session 的工作原理
- 第一次调用
session_start()时,会创建一个随机 id,通过名为PHPSESSID的 cookie 发送给浏览器,并给你一个空的$_SESSION。 - 请求结束时,PHP 把
$_SESSION保存在服务器上(默认存在文件中,也可以存在 Redis 或数据库中)。 - 下一次请求时,浏览器把 cookie 发回来,
session_start()读取 id 并加载保存的数组。
浏览器永远看不到数据,只能看到 id。所以已登录用户的 id 应该放在 $_SESSION 中,绝不能放在你自己设置的 cookie 里:访问者可以把自己的 cookie 改成任意值,但改不了 $_SESSION。
用 session 实现登录和退出登录
登录就是:校验密码,重新生成 session id,保存用户 id。退出登录则相反,分三步。
<?php
// login.php
session_start();
$user = find_user_by_email($_POST['email'] ?? '');
if ($user && password_verify($_POST['password'] ?? '', $user['password_hash'])) {
session_regenerate_id(true); // new id after login: stops session fixation
$_SESSION['user_id'] = $user['id'];
header('Location: /dashboard.php');
exit;
}
$error = 'Wrong email or password.';
<?php
// any protected page
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: /login.php');
exit;
}
<?php
// logout.php
session_start();
$_SESSION = []; // 1. forget the data
$p = session_get_cookie_params(); // 2. delete the cookie
setcookie(session_name(), '', time() - 3600, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
session_destroy(); // 3. delete the stored session
header('Location: /');
exit;
session_regenerate_id(true) 比看上去重要得多。如果攻击者能让受害者使用一个攻击者已知的 session id(比如通过一个链接),而登录后 id 又保持不变,攻击者也就登录进去了。登录时换一个新 id 就能堵上这个漏洞。密码校验本身见 password_hash 页面。
无活动一段时间后让 session 过期
PHP 自带的清理机制(session.gc_maxlifetime,默认 1440 秒)是随机触发的,不是可靠的超时机制。把最后一次请求的时间存起来,自己检查。下面的代码块使用固定的时间戳,方便你看到每种情况;可以改改 $timeout 或时间。
从 09:05 到 09:16:40 间隔 700 秒,没有超过 15 分钟的限制,所以用户保持登录。下一个间隔是 1000 秒,所以 session 被清空。
闪存消息
闪存消息在一次请求中设置,在下一次请求中显示一次,典型的例子是重定向后显示的“已保存”。把它存进 session,然后一步完成读取和删除:
用 setcookie 设置 cookie
setcookie() 接收名称、值和一个选项数组(PHP 7.3 及以上)。除非有特别的理由,否则每个 cookie 都要设置这四个选项:
<?php
setcookie('theme', 'dark', [
'expires' => time() + 60 * 60 * 24 * 30, // 30 days; 0 means "until the browser closes"
'path' => '/', // send it on every page, not just this folder
'secure' => true, // only over HTTPS
'httponly' => true, // JavaScript cannot read it
'samesite' => 'Lax', // not sent on most cross-site requests
]);
$theme = $_COOKIE['theme'] ?? 'light';
有两点容易让人出错:
$_COOKIE在同一个请求中不会改变。 它保存的是浏览器随这次请求发送的内容。现在设置的 cookie,要到下一次请求才会出现在$_COOKIE中。- cookie 是响应头,所以
setcookie()必须在任何输出之前运行,这一点和session_start()、header()完全一样。
要删除一个 cookie,用相同的名称和 path 重新设置它,并把过期时间设为过去:setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/'])。
在 cookie 中保存数组
cookie 的值是一个最多约 4 KB 的字符串,所以结构化数据要以 JSON 形式保存,而且读回来的内容不可信:访问者可以修改它。永远不要对 cookie 调用 unserialize():它可以创建你自己的类的对象并运行它们的魔术方法,而 json_decode() 只会返回数组、字符串、数字、布尔值、null 或 stdClass。
各种标志的用法见 json_encode 页面。如果数据比几个短小的值更多,就把数据放在 session(或数据库)中,cookie 里只保存一个键。
“记住我”令牌
“记住我”cookie 中不能保存用户 id 或密码哈希。在 cookie 中保存一个很长的随机令牌,数据库中只保存它的哈希,并用 hash_equals() 比较:
每次运行的值都不同,因为 random_bytes() 是随机的。即使数据库泄露,没有 cookie 的话这些哈希也毫无用处;hash_equals() 以恒定时间比较,所以校验过程不会泄露时间信息。
session 和 cookie:该用哪个
Session($_SESSION) | Cookie($_COOKIE) | |
|---|---|---|
| 存储位置 | 服务器上 | 浏览器中 |
| 访问者能否读取或修改 | 不能(只能看到 id) | 能 |
| 大小 | 取决于你的存储 | 每个 cookie 约 4 KB |
| 有效期 | 直到浏览器关闭或 session 过期 | 直到它的 expires 时间 |
| 用途 | 已登录用户、购物车、CSRF 令牌、闪存消息 | 主题、语言、同意选项、“记住我”令牌 |
常见错误:headers already sent
session_start()、setcookie() 和 header() 都会发送 HTTP 响应头,而响应头只能在正文之前发送。只要先有任何输出,哪怕是被引入的文件中 <?php 前面的一个空行,都会产生:
Warning: session_start(): Session cannot be started after headers have already been sent in /var/www/page.php on line 3
Warning: Cannot modify header information - headers already sent by (output started at /var/www/page.php:2) in /var/www/page.php on line 4
第二条消息告诉你输出从哪里开始(page.php:2):找到那一行,删掉输出,或者把 session_start() 移到它上面。在只包含 PHP 的文件中,省略结尾的 ?> 可以防止它后面多余的空白变成输出。
常见问题
PHP 中 session 和 cookie 有什么区别?
cookie 保存在访问者的浏览器中,每次请求都会发送,所以访问者可以读取和修改它。session 把数据保存在服务器上;浏览器只在一个 cookie(PHPSESSID)中保存一个随机的 session id。凡是必须可信的内容,比如已登录用户的 id,都应该放在 $_SESSION 中。
PHP 中退出登录时怎么销毁 session?
清空数据,删除 session cookie,再销毁存储的 session:先 $_SESSION = [];,然后 setcookie(session_name(), '', time() - 3600, '/'),最后 session_destroy();。之后再重定向,让下一个页面从干净的状态开始。
PHP 中怎么删除一个 cookie?
用相同的名称、路径和域名重新设置它,并把过期时间设为过去:setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);。如果当前脚本后面还会读取它,再执行 unset($_COOKIE['theme']),因为 $_COOKIE 要到下一个请求才会改变。
PHP session 能保持多久?
默认情况下,session cookie 在浏览器关闭时失效(session.cookie_lifetime = 0),服务器可能会在 1440 秒(24 分钟)无活动后删除 session 数据(session.gc_maxlifetime)。如果需要固定的超时时间,把最后一次请求的时间存进 session,自己检查。
为什么 session_start() 提示 headers already sent?
session id 通过 cookie 头传递,而所有响应头都必须在任何输出之前发送。<?php 之前的 echo、HTML,甚至一个空行或 BOM,都会让输出先发出去。在脚本的最顶部调用 session_start(),在输出任何内容之前。