打开一个网站,过几天回来以后发现自己仍然登录着,或者网站记住了语言、主题和其他个人设置,这背后往往都离不开Cookie。对PHP开发者来说,Cookie并不是什么神秘的“浏览器数据库”,它本质上是一种由浏览器保存、并按照规则自动附加到后续HTTP请求中的少量数据。服务器通过HTTP响应头里的Set-Cookie告诉浏览器保存什么,浏览器以后再通过请求头里的Cookie把符合条件的数据发送回来,PHP最终通过$_COOKIE读取。
最基本的PHP Cookie非常简单:
setcookie('theme', 'dark', time() + 86400, '/');
执行以后,浏览器会收到一个Cookie。以后访问符合条件的页面时,浏览器会自动把它发送给服务器,PHP则可以通过:
$theme = $_COOKIE['theme'] ?? 'light';
读取。如果Cookie只是保存网站显示偏好,这样已经足够。但涉及登录、支付、账户权限等场景,就不能把Cookie当成一个普通变量处理。
首先要理解一个非常容易搞错的概念:Cookie存储在浏览器,Session数据通常存储在服务器。PHP Session常见的工作方式实际上也是浏览器保存一个Session ID,例如PHP通过名为PHPSESSID的Cookie保存会话标识,真正的Session数据则保存在服务器端。因此,“Session一定比Cookie安全”这种说法并不准确。安全性取决于你到底把什么东西放在哪里,以及认证和Cookie属性如何配置。
例如,如果Cookie里面直接保存:
user_id=1001
然后服务器收到这个值以后就直接认为“这是1001号用户”,这种设计存在严重问题。用户完全可以修改自己的Cookie。如果服务器没有进一步验证,攻击者就可能把1001改成1002,从而尝试访问另一个账户。
更合理的设计是Cookie保存一个不可预测的随机会话标识,服务器根据这个标识查找登录会话。例如:
session_id=随机高熵字符串
服务器数据库或Session存储中再记录这个Session对应的用户ID、创建时间、过期时间和其他状态。这样用户修改Cookie中的Session ID也不能直接把自己变成另一个用户,因为有效Session ID应该是不可猜测的随机值。
PHP提供了非常方便的Session机制:
session_start();
$_SESSION['user_id'] = 1001;
$_SESSION['logged_in'] = true;
浏览器通常只保存Session ID,而不是整个$_SESSION数组。后续请求到达服务器后,PHP根据Session ID找到服务器端Session数据。因此,登录系统通常应该优先采用经过设计的Session机制,而不是把用户ID、权限等级等认证信息直接塞进Cookie。
Cookie属性同样非常重要。对于登录Session这类敏感Cookie,通常应该考虑Secure、HttpOnly和SameSite。
例如:
setcookie('session_id', $sessionId, [
'expires' => time() + 86400,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
Secure意味着浏览器只应通过HTTPS发送这个Cookie。在今天的生产网站中,涉及登录凭证的Cookie通常应该启用它。HttpOnly则阻止页面中的JavaScript通过document.cookie读取该Cookie,这可以降低某些XSS攻击窃取Session Cookie的风险。但它并不能阻止XSS本身,也不能把XSS变成“不存在的问题”。
SameSite则控制Cookie在跨站请求场景中的发送行为。Lax通常是许多普通网站登录Session的实际选择;Strict限制更加严格,而None允许跨站发送,但现代浏览器通常要求同时启用Secure。具体设置必须根据网站是否存在跨站登录、第三方嵌入、支付回调等需求决定。
path决定Cookie在哪些URL路径下发送。设置成/意味着整个网站都可以收到它,因此登录Session通常使用根路径。设置成/admin/则可以把Cookie限制在相应路径范围内。
而domain并不是必须设置的参数。很多网站根本不需要它。默认情况下,Cookie属于设置它的主机名,也就是所谓的host-only Cookie。只有确实需要让多个子域名共享Cookie时,才考虑设置Domain。例如某个网站需要让app.example.com和admin.example.com共享同一个Cookie,才可能使用:
setcookie('session_id', $sessionId, [
'expires' => time() + 86400,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
这里还要区分“跨子域”和“跨域”。app.example.com与admin.example.com属于同一个注册域下的不同子域,可以通过Domain属性实现一定程度的Cookie共享;example.com与another-site.com则属于不同站点,不能靠设置一个Domain让Cookie跨到另一个网站。浏览器的同源策略、Cookie规则和SameSite策略都会参与限制。
因此,原稿中所说的“通过document.cookie设置跨域Cookie”是不准确的。JavaScript不能随意把Cookie设置给另一个不属于当前域的站点。跨域应用之间如果需要共享登录状态,通常应该设计正规的认证机制,例如OAuth 2.0、OpenID Connect或服务器之间的Token交换,而不是试图绕过浏览器的Cookie安全边界。
Cookie的生命周期也需要区分Session Cookie和持久Cookie。没有设置明确的expires或Max-Age时,通常属于会话Cookie,其生命周期由浏览器的会话管理决定,并不应该简单理解成“关闭浏览器必然立即删除”。现代浏览器可能存在会话恢复机制,因此不要把它当作严格的安全边界。
需要长期保存的数据则可以设置过期时间,例如用户的非敏感显示偏好。但“登录30天”并不意味着一定应该让一个永久有效的登录凭证存在30天。对于高价值账户,更合理的设计包括Session过期、绝对生命周期、空闲超时、重新认证以及登录状态撤销等机制。
Cookie还有大小限制,因此不适合拿来存储大量数据。实际开发中经常引用“单个Cookie约4KB”作为经验值,但具体限制由浏览器和实现决定,不能把4KB理解成一个所有环境都绝对相同的协议数字。Cookie数量和总大小也有限制,浏览器超过限制后可能出现删除或无法保存等问题。
更重要的是,不要因为Cookie可以保存字符串,就把密码、信用卡信息或者完整用户资料直接放进去。尤其不要认为“使用AES加密以后把敏感数据放Cookie里就安全了”。加密可以解决部分机密性问题,却没有解决Cookie被复制、重放、失效管理、密钥管理以及服务器授权等问题。对于认证凭证,通常更好的方案是让Cookie只携带随机、不可预测的Session标识,而真正的用户状态保存在服务器端。
Cookie还不能替代CSRF防护。SameSite能够降低部分跨站请求风险,但对于修改密码、转账、删除账户等敏感操作,不能简单地认为设置SameSite=Lax以后就万事大吉。根据应用架构,还可能需要CSRF Token、Origin/Referer检查以及重新认证等措施。
PHP操作Cookie还有一个非常实际的问题:HTTP响应头必须在响应发送之前设置。也就是说:
setcookie('theme', 'dark', time() + 86400, '/');
应该发生在输出HTML之前。如果代码已经输出内容,甚至已经因为BOM、空格或错误信息导致响应头发送,再调用setcookie()就可能出现“headers already sent”错误。
删除Cookie也不是简单调用一个“deleteCookie”函数。通常需要使用与原Cookie匹配的路径和Domain,并把过期时间设置到过去:
setcookie('theme', '', [
'expires' => time() - 3600,
'path' => '/',
]);
如果当初设置Cookie时使用了不同的Domain或Path,删除时参数不匹配,就可能出现“明明删除了但浏览器里面还存在”的情况,因为实际上操作的是另一个Cookie。
调试Cookie时,现代浏览器开发者工具中的Application或Storage区域非常有用,可以直接查看Cookie的Domain、Path、Expires、Secure、HttpOnly和SameSite等属性。Network面板则更加重要,因为它可以观察服务器到底发送了什么Set-Cookie响应头,以及后续请求到底有没有带上Cookie。遇到“Cookie没有生效”,不要只看$_COOKIE,应该从HTTP请求和响应两端同时检查。
还需要纠正一个常见说法:Cookie值并不是简单地“传输过程中自动进行URL编码,所以必须使用rawurlencode()”。PHP的setcookie()对Cookie值有自己的处理规则,开发者不应该无条件再进行一次URL编码,否则读取时可能得到经过双重编码的数据。真正需要保存复杂结构时,应明确采用可靠的序列化和编码方式,并理解Cookie允许的字符规则,而不是见到Cookie就习惯性调用urlencode()。
如果网站使用HTTPS,还应该关注HSTS、TLS配置和整个登录链路的安全性。因为Secure只能告诉浏览器不要通过HTTP发送这个Cookie,并不能修复一个本身仍然允许HTTP登录、存在XSS或者Session固定攻击漏洞的网站。
登录系统还有一个非常重要的步骤叫Session Rotation。用户成功登录以后,服务器应该重新生成Session ID,避免攻击者提前让受害者使用一个已知Session ID,再等待受害者登录后接管会话。PHP可以使用:
session_regenerate_id(true);
在适当的登录流程中更新Session ID。
最终可以把PHP Cookie理解成一个“浏览器帮服务器携带的小型状态标识”。它非常方便,但它本身并不负责判断用户是不是好人,也不负责判断用户有没有权限。Cookie只是运输工具,真正的安全边界来自服务器端的认证、授权、Session管理、CSRF防护、XSS防护以及合理的Cookie属性。
一个成熟的PHP登录系统通常不是“把user_id写进Cookie”这么简单,而是用户登录后生成不可预测的Session ID,Cookie使用Secure、HttpOnly和合适的SameSite策略,服务器保存真正的会话状态,敏感操作进行授权检查,并在登录、登出、密码修改等关键节点正确轮换或撤销Session。这样,Cookie才真正成为网站“记住用户”的可靠基础,而不是一个藏在浏览器里的可篡改身份标签。
PHP Cookie 怎么使用?网站记住用户状态时需要注意哪些问题
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP