HTTP 协议详解教程 / 第 7 章:Cookie 机制
第 7 章:Cookie 机制
Cookie 是 HTTP 无状态特性的核心补充机制。理解 Cookie 的工作原理和安全属性,对构建安全的 Web 应用至关重要。
7.1 Cookie 基础
什么是 Cookie
Cookie 是服务器通过 HTTP 响应头 Set-Cookie 设置的小型数据,浏览器会在后续请求中自动携带。
工作流程
┌─────────┐ ┌─────────┐
│ Browser │ │ Server │
└────┬────┘ └────┬────┘
│ │
│ ─── 1. POST /login ────────→│
│ │── 验证凭据
│ ←── 2. Set-Cookie: session=abc ─│
│ │
│ ─── 3. GET /profile ───────→│
│ Cookie: session=abc │── 读取 Cookie
│ ←── 4. 200 OK ──────────── │
│ │
7.2 Set-Cookie 响应头
语法
Set-Cookie: name=value[; attribute1=value1][; attribute2=value2]...
完整示例
Set-Cookie: session_id=abc123; Domain=example.com; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax
属性详解
| 属性 | 说明 | 示例 |
|---|
| name=value | Cookie 名称和值 | session=abc123 |
| Domain | 适用域名 | .example.com(含子域名) |
| Path | 适用路径 | /api |
| Max-Age | 有效期(秒) | 3600(1小时) |
| Expires | 过期时间点 | Wed, 10 May 2027 10:00:00 GMT |
| HttpOnly | 禁止 JavaScript 访问 | HttpOnly |
| Secure | 仅 HTTPS 传输 | Secure |
| SameSite | 跨站限制 | Strict / Lax / None |
7.3 Cookie 属性详解
Domain 属性
# 设置 example.com 及所有子域名
Set-Cookie: id=abc; Domain=example.com; Path=/
# 只设置当前域名(默认)
Set-Cookie: id=abc; Path=/
| 设置 | www.example.com | api.example.com | other.com |
|---|
Domain=example.com | ✓ | ✓ | ✗ |
Domain=www.example.com | ✓ | ✗ | ✗ |
| 无 Domain | ✓ | ✗ | ✗ |
📝 注意:Domain 不能设置为公共后缀(如 .com、.co.uk)。
Path 属性
# 只在 /api 路径下发送
Set-Cookie: api_token=xyz; Path=/api
# 在所有路径下发送
Set-Cookie: session=abc; Path=/
| 设置 | / | /api | /api/users | /admin |
|---|
Path=/ | ✓ | ✓ | ✓ | ✓ |
Path=/api | ✗ | ✓ | ✓ | ✗ |
Path=/api/users | ✗ | ✗ | ✓ | ✗ |
Max-Age vs Expires
# Max-Age(推荐)— 相对时间
Set-Cookie: session=abc; Max-Age=3600
# Expires — 绝对时间
Set-Cookie: session=abc; Expires=Wed, 10 May 2027 10:00:00 GMT
# 两者都没有 — 会话 Cookie(浏览器关闭时删除)
Set-Cookie: session=abc
| 特性 | Max-Age | Expires |
|---|
| 格式 | 秒数 | HTTP 日期 |
| 优先级 | 更高 | 较低(Max-Age 存在时被忽略) |
| 推荐 | ✓ | 旧浏览器兼容 |
HttpOnly 属性
# HttpOnly — JavaScript 无法访问
Set-Cookie: session=abc123; HttpOnly; Path=/
# 无 HttpOnly — JavaScript 可读取
Set-Cookie: theme=dark; Path=/
// JavaScript 访问 Cookie
console.log(document.cookie);
// 输出: "theme=dark" (不包含 HttpOnly 的 session)
// 无法读取 HttpOnly Cookie
// 这是防止 XSS 窃取会话的关键
⚠️ 安全提示:会话 Cookie 必须设置 HttpOnly,防止 XSS 攻击窃取。
Secure 属性
# 仅通过 HTTPS 发送
Set-Cookie: session=abc; Secure; HttpOnly
| 协议 | 发送 Secure Cookie |
|---|
| HTTP | ✗ |
| HTTPS | ✓ |
SameSite 属性
SameSite 控制跨站请求是否携带 Cookie,是防御 CSRF 的关键。
| 值 | 跨站请求 | 顶级导航 | 使用场景 |
|---|
| Strict | ✗ 不发送 | ✗ 不发送 | 最严格,需要重新登录 |
| Lax(默认) | ✗ 不发送 | ✓ GET 发送 | 推荐默认值 |
| None | ✓ 发送 | ✓ 发送 | 跨站场景(必须 Secure) |
# Strict — 完全不跨站
Set-Cookie: session=abc; SameSite=Strict; Secure
# Lax — 导航可以,API 不行(默认)
Set-Cookie: session=abc; SameSite=Lax; Secure
# None — 允许跨站(必须 Secure)
Set-Cookie: tracker=xyz; SameSite=None; Secure
7.4 安全最佳实践
安全会话 Cookie 配置
Set-Cookie: session_id=eyJhbG...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
| 属性 | 设置 | 原因 |
|---|
| HttpOnly | ✓ | 防止 XSS 窃取 |
| Secure | ✓ | 防止中间人截获 |
| SameSite | Lax | 防止 CSRF |
| Path | / | 全站可用 |
| Max-Age | 3600 | 1 小时过期 |
Python 服务端设置
from http.server import HTTPServer, BaseHTTPRequestHandler
class SecureHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/login':
self.send_response(200)
# 安全的会话 Cookie
self.send_header('Set-Cookie',
'session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600')
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(b'Login successful')
elif self.path == '/profile':
cookies = self.headers.get('Cookie', '')
if 'session=abc123' in cookies:
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(b'Welcome back!')
else:
self.send_response(401)
self.end_headers()
self.wfile.write(b'Please login')
Express.js Cookie 管理
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser('secret-key'));
app.post('/login', (req, res) => {
// 验证凭据...
// 设置安全 Cookie
res.cookie('session', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600 * 1000, // 毫秒
path: '/'
});
res.json({ message: '登录成功' });
});
app.get('/profile', (req, res) => {
const session = req.cookies.session;
if (!session) {
return res.status(401).json({ error: '请先登录' });
}
// 读取会话数据...
res.json({ user: 'alice' });
});
app.post('/logout', (req, res) => {
res.clearCookie('session', {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/'
});
res.json({ message: '已退出' });
});
7.5 Session 管理
服务端 Session
客户端 服务器
│ │
│── POST /login ──────────→│
│ │── 验证密码
│ │── 创建 Session: {user: "alice"}
│ │── Session ID: sess_abc123
│← Set-Cookie: session=sess_abc123 ─│
│ │
│── GET /profile ──────────→│
│ Cookie: session=sess_abc123 │
│ │── 查找 Session: sess_abc123
│ │── 找到: {user: "alice"}
│← 200 {"user": "alice"} ──│
常见 Session 存储方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|
| 内存 | 最简单 | 进程重启丢失 | 开发环境 |
| 文件 | 持久化 | 不适合分布式 | 小型应用 |
| Redis | 快速、支持分布式 | 需要额外服务 | 生产环境推荐 |
| 数据库 | 持久化 | 相对较慢 | 需要审计的场景 |
Redis Session 示例
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: 'redis://localhost:6379' });
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600 * 1000
},
name: 'session_id' // 自定义 Cookie 名
}));
app.post('/login', (req, res) => {
// 验证后创建 Session
req.session.user = { id: 1, name: 'alice' };
req.session.loginTime = new Date();
res.json({ message: '登录成功' });
});
app.get('/profile', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: '请先登录' });
}
res.json({ user: req.session.user });
});
7.6 Cookie vs Session vs Token
| 特性 | Cookie | 服务端 Session | JWT Token |
|---|
| 存储位置 | 浏览器 | 服务端 | 客户端 |
| 有状态 | 否(仅存储) | 是 | 否 |
| 可伸缩性 | 高 | 需要共享存储 | 高 |
| CSRF 风险 | 有 | 有 | 低 |
| XSS 风险 | HttpOnly 保护 | HttpOnly 保护 | 存储在 JS 中有风险 |
| 移动端 | 不方便 | 不方便 | 方便 |
| 跨域 | 受限 | 受限 | 灵活 |
7.7 CSRF 防护
CSRF 攻击原理
1. 用户登录 bank.com,获得 session Cookie
2. 用户访问恶意网站 evil.com
3. evil.com 向 bank.com 发送请求,浏览器自动携带 Cookie
4. bank.com 认为是合法请求,执行操作
防护方案
// 1. SameSite Cookie(最简单)
res.cookie('session', 'abc', { sameSite: 'lax' });
// 2. CSRF Token
const crypto = require('crypto');
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// 生成 Token
app.get('/form', (req, res) => {
const csrfToken = generateCSRFToken();
req.session.csrfToken = csrfToken;
res.render('form', { csrfToken });
});
// 验证 Token
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF token 验证失败' });
}
// 处理转账...
});
7.8 业务场景:电商网站会话管理
const express = require('express');
const app = express();
// 会话 Cookie 配置
const sessionConfig = {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/'
};
// 登录
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticate(email, password);
if (!user) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
// 创建会话
const sessionId = await createSession(user);
res.cookie('session', sessionId, {
...sessionConfig,
maxAge: 24 * 3600 * 1000 // 24 小时
});
// 记住我
if (req.body.remember) {
res.cookie('remember_token', generateRememberToken(user), {
...sessionConfig,
maxAge: 30 * 24 * 3600 * 1000 // 30 天
});
}
res.json({ user: { id: user.id, name: user.name } });
});
// 退出
app.post('/api/logout', (req, res) => {
const sessionId = req.cookies.session;
if (sessionId) {
deleteSession(sessionId);
}
res.clearCookie('session', sessionConfig);
res.clearCookie('remember_token', sessionConfig);
res.json({ message: '已退出' });
});
// 认证中间件
app.use('/api/*', async (req, res, next) => {
const sessionId = req.cookies.session;
if (!sessionId) {
return res.status(401).json({ error: '请先登录' });
}
const session = await getSession(sessionId);
if (!session) {
res.clearCookie('session', sessionConfig);
return res.status(401).json({ error: '会话已过期,请重新登录' });
}
req.user = session.user;
next();
});
⚠️ 注意事项
- HttpOnly 必须设置:会话 Cookie 必须使用 HttpOnly 防止 XSS
- Secure 必须设置:生产环境必须使用 Secure
- SameSite 默认 Lax:大多数场景 Lax 是最佳选择
- Cookie 大小限制:每个 Cookie 约 4KB,每个域名约 50 个 Cookie
- 不要存储敏感信息:Cookie 值应是随机 Session ID,不是用户数据
- 及时清除:退出时清除 Cookie 和服务端 Session
🔗 扩展阅读
下一章:第 8 章:HTTP 缓存 — 强缓存/协商缓存、ETag、Last-Modified、缓存策略设计