强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 7 章:Cookie 机制

第 7 章:Cookie 机制

Cookie 是 HTTP 无状态特性的核心补充机制。理解 Cookie 的工作原理和安全属性,对构建安全的 Web 应用至关重要。


Cookie 是服务器通过 HTTP 响应头 Set-Cookie 设置的小型数据,浏览器会在后续请求中自动携带。

工作流程

┌─────────┐                    ┌─────────┐
│ Browser │                    │ Server  │
└────┬────┘                    └────┬────┘
     │                              │
     │ ─── 1. POST /login ────────→│
     │                              │── 验证凭据
     │ ←── 2. Set-Cookie: session=abc ─│
     │                              │
     │ ─── 3. GET /profile ───────→│
     │     Cookie: session=abc     │── 读取 Cookie
     │ ←── 4. 200 OK ──────────── │
     │                              │

语法

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=valueCookie 名称和值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

Domain 属性

# 设置 example.com 及所有子域名
Set-Cookie: id=abc; Domain=example.com; Path=/

# 只设置当前域名(默认)
Set-Cookie: id=abc; Path=/
设置www.example.comapi.example.comother.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-AgeExpires
格式秒数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 安全最佳实践

Set-Cookie: session_id=eyJhbG...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
属性设置原因
HttpOnly防止 XSS 窃取
Secure防止中间人截获
SameSiteLax防止 CSRF
Path/全站可用
Max-Age36001 小时过期

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')
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 });
});

特性Cookie服务端 SessionJWT 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();
});

⚠️ 注意事项

  1. HttpOnly 必须设置:会话 Cookie 必须使用 HttpOnly 防止 XSS
  2. Secure 必须设置:生产环境必须使用 Secure
  3. SameSite 默认 Lax:大多数场景 Lax 是最佳选择
  4. Cookie 大小限制:每个 Cookie 约 4KB,每个域名约 50 个 Cookie
  5. 不要存储敏感信息:Cookie 值应是随机 Session ID,不是用户数据
  6. 及时清除:退出时清除 Cookie 和服务端 Session

🔗 扩展阅读


下一章第 8 章:HTTP 缓存 — 强缓存/协商缓存、ETag、Last-Modified、缓存策略设计