在浏览器存储体系中,Cookie 是最古老、也最广为人知的机制。尽管后来出现了 Web Storage、IndexedDB 等更强大的方案,Cookie 依然在身份认证、用户偏好、跟踪分析等场景中扮演着不可替代的角色。理解它的运作方式,是前端工程师必须掌握的基本功。
17.1.1 Cookie 的基本原理
Cookie 本质上是服务端通过 HTTP 响应头指令浏览器存储的一小段文本数据,浏览器会在后续对同一服务器的请求中自动携带这段数据。这种“服务器设置、浏览器保管、请求时自动回传”的模式,让 HTTP 这种无状态协议得以维持会话状态。
整个交互流程如下:
- 浏览器发起请求,服务器在响应头中附带一个或多个
Set-Cookie字段。 - 浏览器接收到响应后,将 Cookie 按照约定规则存储到本地。
- 之后每次向该服务器发送请求时,浏览器自动将符合条件的 Cookie 附加到请求头中的
Cookie字段里。
服务端设置 Cookie(Node.js 示例)
// 在 HTTP 响应头中设置 Cookie
res.setHeader('Set-Cookie', 'sessionId=abc123; HttpOnly; Secure; SameSite=Strict');
客户端读取请求头(浏览器自动携带,不可手动干预读取 Set-Cookie)
实际上,浏览器自动管理 Cookie,服务端收到的请求头可能是这样的:
Cookie: sessionId=abc123; theme=dark
前端 JavaScript 可以通过 document.cookie 读取和修改 Cookie,但只能访问那些非 HttpOnly 的 Cookie。这为客户端操作提供了可能性,但也埋下了安全隐患。
17.1.2 Cookie 的核心属性
Cookie 的威力(和复杂性)不在于它存储了值,而在于它附带的那些控制属性——它们精准定义了 Cookie 的发送范围、有效期和访问权限。
Domain —— 控制 Cookie 发送到哪些域
Domain 属性指定了 Cookie 可以送达的主机。如果不设置,默认是当前文档域名(不包括子域)。如果设置了 Domain=mozilla.org,那么该 Cookie 对 mozilla.org 及其所有子域(如 developer.mozilla.org)都有效。
// 允许 .example.com 下的所有子域共享该 Cookie
Set-Cookie: user=john; Domain=example.com
注意:不能将 Domain 设置为顶级域(如 .com)或其他你无权控制的域,浏览器会忽略它。
Path —— 控制 Cookie 发送到哪些路径
Path 属性限制 Cookie 仅对指定路径及其子路径有效。默认是当前请求路径的目录部分。比如设置 Path=/docs,那么只有路径以 /docs 开头的请求才会携带该 Cookie。
Set-Cookie: pref=dark; Path=/app
Expires / Max-Age —— 控制 Cookie 的生命周期
这两个属性都用于定义 Cookie 的有效期:
Expires采用绝对时间(GMT 格式),到达指定日期后 Cookie 被删除。Max-Age使用相对秒数,从当前时间开始计算。推荐使用Max-Age,它更简洁且不易受时区影响。
如果不设置任何过期时间,Cookie 默认是会话级 Cookie,浏览器关闭时即被删除。
// 设置 7 天后过期
Set-Cookie: token=xyz; Max-Age=604800
Secure —— 限制仅 HTTPS 连接传输
带有 Secure 标记的 Cookie 只在 HTTPS 请求中发送,明文 HTTP 请求不会携带该 Cookie。这能有效防止中间人攻击窃取敏感信息。
Set-Cookie: session=123; Secure
强制建议:任何包含身份标识或敏感数据的 Cookie 都应该加上 Secure 标记。在现代 Web 应用中,全站 HTTPS 是基本要求。
HttpOnly —— 禁止 JavaScript 访问
启用了 HttpOnly 的 Cookie,无法通过 document.cookie 读取或修改,只能由浏览器自动在请求中携带。这是防范 XSS 攻击窃取身份 Cookie 的最有效手段。
Set-Cookie: sessionId=secret; HttpOnly
一条实用原则:凡是服务端颁发的会话 Token,一律设为 HttpOnly,让前端脚本无从窥探。
SameSite —— 防御 CSRF 的核心利器
SameSite 属性控制 Cookie 是否随着跨站请求被发送,它有三个取值:
- Strict(最严格):完全禁止在跨站请求中携带该 Cookie。即使点击来自其他站点的链接,首次进入时也不会携带,可能导致用户每次从外部链接进来都需要重新登录。
- Lax(推荐的平衡值):允许在安全的跨站 GET 请求(如点击链接)中携带,但阻止 POST、PUT 等方法的跨站携带。对大多数常见场景既安全又不影响基本体验。
- None:跨站请求一律携带,但必须同时设置
Secure属性(否则浏览器会拒绝)。
Set-Cookie: session=abc; SameSite=Lax; Secure
Chrome 等现代浏览器已将 SameSite=Lax 设为默认值,有效压制了大部分 CSRF 攻击。但仍需根据实际需求显式设置。
17.1.3 Cookie 的限制与缺点
大小与数量限制
浏览器对单个 Cookie 的大小通常限制在 4KB 左右,每个域下的 Cookie 总数也有限制(不同浏览器约 20~50 个)。这意味着 Cookie 只适合存放少量关键信息(如会话 ID),绝不能当作本地数据库使用。
每次请求自动携带
这是 Cookie 最大的“双刃剑”。一方面它省去了手动添加身份令牌的麻烦;另一方面,每个请求都会包含域下所有符合条件的 Cookie,哪怕请求的是静态图片或 CSS 文件。这会增加无效的传输开销,特别是当 Cookie 体积较大时,可能严重影响页面加载性能。
同源限制与跨域
Cookie 遵循同源策略,但它的作用域判定有其特殊规则:与 document.domain、Domain 属性、端口和协议相关。默认情况下,Cookie 不能跨域共享,但可以通过设置 Domain 实现在子域间共享(如 a.example.com 和 b.example.com)。需要特别注意,SameSite 属性会进一步影响跨站行为。
17.1.4 安全策略最佳实践
Cookie 是 Web 安全攻防的一个焦点。遵循以下准则能大幅降低风险:
- 凡是服务端会话 Token,一律设为 HttpOnly + Secure + SameSite=Lax,阻止脚本读取和跨站伪造。
- 合理控制 Domain 和 Path,尽可能缩小 Cookie 的作用范围,避免将敏感 Cookie 扩散到不必要的子域或路径。
- 精简 Cookie 体积,不要在 Cookie 中存放冗余信息;使用专门的空间(如
id_token只存 ID,详细信息可以服务端查询)。 - 配合 CSRF Token 或自定义请求头,为敏感操作(转账、改密)额外增加防护层。
- 定期审计遗留 Cookie,移除不再使用的旧 Cookie,避免成为攻击跳板。
- 使用
Prefix标签增强安全性:
__Host-前缀要求 Cookie 必须同时设置Secure、Path=/且无Domain属性,防止子域篡改。__Secure-前缀要求Secure且来自安全页面。
Set-Cookie: __Host-session=abc; Secure; Path=/; SameSite=Lax
17.1.5 开发者日常操作示例
通过 JavaScript 读取/写入 Cookie(仅非 HttpOnly)
// 设置一个 7 天有效的普通 Cookie
document.cookie = "theme=dark; Max-Age=604800; path=/";
// 读取所有 Cookie 并解析成对象
const cookies = document.cookie.split('; ').reduce((acc, cur) => {
const [key, ...rest] = cur.split('=');
acc[key] = rest.join('=');
return acc;
}, {});
console.log(cookies.theme); // 'dark'
服务端删除 Cookie 的方法
将有效期设置为一个过去的时间即可:
res.setHeader('Set-Cookie', 'oldCookie=; Max-Age=0');
Cookie 虽小,却承载着 HTTP 协议中最核心的状态管理职责。在下一节中,我们将看到 Web Storage 如何解决了 Cookie 在存储容量、传输效率上的局限,成为现代 Web 应用的重要补充。