一、Cookie 和 Session 的存储内容对比
- Cookie 中存储什么信息
本质:Cookie 只存储一个 Session ID(会话标识符)
具体内容:
javascript
// 一个典型的 Cookie 示例(开发者工具中查看)
Name: sessionId
Value: s%3Aabc123def456.session_secret_signature
// 解码后:s:abc123def456.session_secret_signature
// s: 是 express-session 的前缀,abc123def456 是真正的 session ID
// Cookie 的其他关键字段:
Domain: .example.com // 作用域
Path: / // 路径
Expires/Max-Age: 时间戳 // 过期时间
Secure: true // 仅 HTTPS
HttpOnly: true // 防止 JS 访问
SameSite: Lax // 跨站策略
为什么 Cookie 只存 ID?
安全考虑:Cookie 存储在用户浏览器,可能被窃取
大小限制:Cookie 一般限制在 4KB
网络性能:每次请求都会自动发送 Cookie
- Session 中存储什么信息
本质:Session 存储在服务器端,保存用户的完整会话数据
具体内容:
javascript
// 一个典型的 session 数据结构(存储在 Redis 中)
{
"sess:abc123def456": {
"userId": "123456",
"username": "john_doe",
"email": "john@example.com",
"roles": ["user", "admin"],
"loginTime": "2024-01-15T10:30:00Z",
"lastActivity": "2024-01-15T11:15:00Z",
"cart": [
{ "productId": "p001", "quantity": 2 },
{ "productId": "p002", "quantity": 1 }
],
"preferences": {
"theme": "dark",
"language": "zh-CN",
"notifications": true
},
"csrfToken": "x678y9z0", // 防止 CSRF 攻击
"_expire": 1705313700000 // 过期时间戳
}
}
Session 中常见的数据类型:
认证信息:userId, username, email
权限信息:roles, permissions
会话状态:loginTime, lastActivity
业务数据:购物车、表单草稿、临时数据
安全令牌:csrfToken, oauthState
用户偏好:主题、语言设置
二、存储位置详解
- Cookie 存储位置
位置:客户端浏览器中
具体存储:
text
浏览器存储位置:
├── 内存 Cookie(Session Cookie)
│ └── 浏览器关闭即删除
│
├── 硬盘 Cookie(持久 Cookie)
│ └── 按过期时间存储在硬盘
│
└── 按域和路径组织
├── example.com/
│ ├── sessionId
│ ├── userPref
│ └── trackingId
└── api.example.com/
└── authToken
查看方式(浏览器开发者工具):
Chrome: Application → Storage → Cookies
Firefox: Storage → Cookies
命令行: document.cookie(仅限非 HttpOnly Cookie)
- Session 存储位置
位置:服务器端
存储介质:
javascript
// 1. 内存存储(默认,不推荐生产环境)
const session = require('express-session');
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}));
// 2. Redis 存储(推荐生产环境)
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({
host: 'localhost',
port: 6379,
prefix: 'sess:', // Redis key 前缀
ttl: 86400 // 过期时间(秒)
}),
secret: 'your-secret'
}));
// 3. 数据库存储(MySQL/PostgreSQL/MongoDB)
const MongoStore = require('connect-mongo');
app.use(session({
store: MongoStore.create({
mongoUrl: 'mongodb://localhost/sessions',
ttl: 14 * 24 * 60 * 60 // 14天
}),
secret: 'your-secret'
}));
Redis 中的实际存储结构:
bash
# 查看所有 session
redis-cli keys "sess:*"
# 查看具体 session 内容
redis-cli get "sess:abc123def456"
# 返回 JSON 字符串,包含所有 session 数据
三、完整实现流程详解
流程图
text
客户端 服务器
| |
|--- 1. 登录请求 --------->|
| |
|<-- 2. 创建 session -----|
| Set-Cookie: sessionId|
| |
|--- 3. 带 Cookie 请求 --->|
| Cookie: sessionId |
| |
|<-- 4. 验证 session -----|
| 返回 session 数据 |
| |
|--- 5. 后续请求 --------->|
| (自动携带 Cookie) |
| |
|<-- 6. 验证/更新 session |
| |
|--- 7. 登出请求 --------->|
| |
|<-- 8. 销毁 session -----|
| 清除 Cookie |
详细步骤代码实现
javascript
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const app = express();
// 1. Session 中间件配置
app.use(session({
store: new RedisStore({
host: '127.0.0.1',
port: 6379,
prefix: 'sess:',
ttl: 1800 // 30分钟
}),
name: 'sessionId', // Cookie 名称,默认 'connect.sid'
secret: 'complex-secret-key-change-in-production',
resave: false, // 避免 session 被覆盖
saveUninitialized: false, // 不保存空的 session
cookie: {
// Cookie 相关设置
maxAge: 30 * 60 * 1000, // 30分钟(毫秒)
secure: process.env.NODE_ENV === 'production', // 仅 HTTPS
httpOnly: true, // 防止 XSS
sameSite: 'lax', // CSRF 防护
path: '/', // Cookie 路径
domain: '.example.com' // 作用域
},
rolling: true, // 每次请求重置过期时间
genid: function(req) {
// 生成唯一的 session ID
return require('crypto').randomBytes(16).toString('hex');
}
}));
// 2. 用户登录 - 创建 session
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 验证用户凭证
const user = await authenticateUser(username, password);
if (user) {
// 在 session 中存储用户信息
req.session.userId = user.id;
req.session.username = user.username;
req.session.roles = user.roles;
req.session.loginTime = new Date();
req.session.lastActivity = new Date();
// 生成 CSRF token
req.session.csrfToken = require('crypto')
.randomBytes(32).toString('hex');
// 设置购物车(如果之前有)
if (!req.session.cart) {
req.session.cart = [];
}
// 发送响应(Cookie 会自动通过 Set-Cookie 头发送)
res.json({
success: true,
message: '登录成功',
user: {
id: user.id,
username: user.username
},
// 如果需要,可以在响应中返回 CSRF token
csrfToken: req.session.csrfToken
});
} else {
res.status(401).json({ success: false, message: '认证失败' });
}
});
// 3. Session 验证中间件
function requireAuth(req, res, next) {
// 检查 session 是否存在且包含用户信息
if (!req.session || !req.session.userId) {
return res.status(401).json({
error: '未授权,请先登录'
});
}
// 更新最后活动时间
req.session.lastActivity = new Date();
// 检查 CSRF token(对于 POST/PUT/DELETE 请求)
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const clientToken = req.headers['x-csrf-token'] || req.body._csrf;
if (clientToken !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF token 无效' });
}
}
// 将用户信息附加到请求对象
req.user = {
id: req.session.userId,
username: req.session.username,
roles: req.session.roles
};
next();
}
// 4. 受保护的路由
app.get('/api/profile', requireAuth, (req, res) => {
// req.user 已在中间件中设置
res.json({
user: req.user,
loginTime: req.session.loginTime,
lastActivity: req.session.lastActivity
});
});
// 5. 更新 session 数据(如购物车)
app.post('/api/cart/add', requireAuth, (req, res) => {
const { productId, quantity } = req.body;
// 确保购物车存在
if (!req.session.cart) {
req.session.cart = [];
}
// 查找商品是否已在购物车
const existingItem = req.session.cart.find(
item => item.productId === productId
);
if (existingItem) {
existingItem.quantity += quantity;
} else {
req.session.cart.push({ productId, quantity });
}
// 手动保存 session(当修改了 session 时)
req.session.save((err) => {
if (err) {
console.error('保存 session 失败:', err);
return res.status(500).json({ error: '服务器错误' });
}
res.json({
success: true,
cart: req.session.cart
});
});
});
// 6. 用户登出 - 销毁 session
app.post('/api/logout', (req, res) => {
// 获取 session ID(用于清理其他存储)
const sessionId = req.session.id;
// 销毁 session
req.session.destroy((err) => {
if (err) {
console.error('销毁 session 失败:', err);
return res.status(500).json({ error: '登出失败' });
}
// 清除客户端的 Cookie
res.clearCookie('sessionId', {
path: '/',
domain: '.example.com'
});
// 可选:清理其他相关存储
cleanupSessionData(sessionId);
res.json({ success: true, message: '登出成功' });
});
});
// 7. Session 清理任务(定时任务)
function cleanupExpiredSessions() {
// Redis 会自动清理过期的 session(基于 TTL)
// 对于数据库存储,可能需要定时任务
setInterval(async () => {
const expiredSessions = await SessionModel.find({
expires: { $lt: new Date() }
});
// 清理过期 session
await SessionModel.deleteMany({
_id: { $in: expiredSessions.map(s => s._id) }
});
console.log(`清理了 ${expiredSessions.length} 个过期 session`);
}, 3600000); // 每小时清理一次
}
四、关键字段和作用详解
Cookie 字段配置表
| 字段 | 值示例 | 作用 | 重要性 | 详细说明 |
|---|---|---|---|---|
| Name | sessionId | Cookie 名称 | ★★★ | 标识 cookie 的键名,服务器通过此名称读取 cookie 值 |
| Value | s%3Aabc123 | 加密的 session ID | ★★★ | 经过编码和签名的 session ID,是连接客户端和服务器的关键凭证 |
| Domain | .example.com | 作用域 | ★★★ | 指定 cookie 有效的域名,. 开头表示所有子域名共享 |
| Path | / | Cookie 有效的路径 | ★★ | 指定 cookie 在网站中的有效路径,/ 表示全站有效 |
| Expires/Max-Age | 2024-01-16T10:30:00Z | 过期时间 | ★★★ | 控制 cookie 的存活时间,过期后浏览器自动删除 |
| Secure | true | 仅通过 HTTPS 传输 | ★★★ | 防止 cookie 在明文中传输被窃取,生产环境必须启用 |
| HttpOnly | true | 禁止 JavaScript 访问 | ★★★ | 防止 XSS 攻击窃取 cookie,敏感 cookie 必须设置 |
| SameSite | Lax/Strict | CSRF 防护策略 | ★★★ | 控制跨站请求是否携带 cookie,有效防止 CSRF 攻击 |
Session 配置字段表
| 字段 | 默认值 | 作用 | 推荐设置 | 详细说明 |
|---|---|---|---|---|
| name | connect.sid | Cookie 名称 | 自定义名称 | 避免使用默认名称,提高安全性,防止攻击者猜测 |
| secret | - | 签名密钥 | 长且复杂的随机字符串 | 用于签名 session ID,防止篡改,定期更换 |
| resave | true | 强制保存 session | false | 避免每次请求都保存 session,减少竞争条件 |
| saveUninitialized | true | 保存空 session | false | 不保存未修改的 session,节省存储空间 |
| cookie.secure | false | 仅 HTTPS | 生产环境设为 true | 确保 cookie 只在安全连接中传输 |
| cookie.httpOnly | true | 防 XSS | 始终 true | 防止 JavaScript 访问敏感 cookie |
| cookie.sameSite | false | 跨站策略 | lax 或 strict | strict:完全禁止跨站;lax:部分允许 |
| cookie.maxAge | null | Cookie 过期时间 | 根据业务设置 | 单位毫秒,如 30 分钟:30 * 60 * 1000 |
| rolling | false | 重置过期时间 | true | 用户每次活动都延长 session 有效期 |
| store | 内存 | Session 存储 | Redis(生产环境) | 生产环境必须使用外部存储,支持集群部署 |
Session 数据字段表
| 字段名 | 类型 | 存储内容 | 生命周期 | 详细说明 |
|---|---|---|---|---|
| userId | String | 用户唯一标识 | 登录到登出 | 用于关联数据库中的用户记录,最关键的字段 |
| username | String | 用户名 | 登录到登出 | 显示用途,避免频繁查询数据库 |
| roles | Array | 用户角色权限 | 登录到登出 | 如 ["user", "admin"],用于权限控制 |
| loginTime | Date | 登录时间 | 登录到登出 | 记录用户登录时刻,用于审计和安全分析 |
| lastActivity | Date | 最后活动时间 | 每次请求更新 | 用于判断用户是否活跃,实现自动登出 |
| csrfToken | String | CSRF 防护令牌 | 每次会话 | 随机生成的 token,防止跨站请求伪造攻击 |
| cart | Array | 购物车数据 | 会话期间 | 存储用户临时的购物车信息,如 [{productId, quantity}] |
| preferences | Object | 用户偏好 | 会话期间 | 如主题、语言等个性化设置 |
| tempData | Any | 临时数据 | 短期存储 | 表单数据、验证码等临时信息,定期清理 |
五、安全最佳实践
- Cookie 安全设置
javascript
app.use(session({
// ... 其他配置
cookie: {
secure: true, // 强制 HTTPS
httpOnly: true, // 防 XSS
sameSite: 'strict', // 防 CSRF
domain: '.yourdomain.com', // 限制域名
path: '/', // 限制路径
maxAge: 30 * 60 * 1000 // 合理过期时间
}
}));
- Session 数据清理
javascript
// 定期清理过期 session
function cleanOldSessions() {
// 删除超过30天未活动的 session
const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000);
// Redis 示例
redisClient.keys('sess:*', (err, keys) => {
keys.forEach(key => {
redisClient.get(key, (err, sessionStr) => {
if (sessionStr) {
const session = JSON.parse(sessionStr);
if (session.lastActivity < cutoff) {
redisClient.del(key);
}
}
});
});
});
}
- 敏感信息处理
javascript
// 不要在 session 中存储敏感信息
// 错误示例:
req.session.passwordHash = user.passwordHash; // ❌ 危险!
// 正确做法:
req.session.userId = user.id; // ✅ 只存储标识符
req.session.roles = user.roles; // ✅ 存储非敏感信息
// 敏感信息从数据库实时获取
function getUserData(req, res, next) {
if (req.session.userId) {
User.findById(req.session.userId)
.select('-password -salt') // 排除敏感字段
.then(user => {
req.user = user;
next();
});
} else {
next();
}
}
六、常见问题排查
- Session 不持久
avascript
// 检查点:
// 1. Cookie 是否设置正确
console.log('Cookie:', req.headers.cookie);
// 2. Session 是否保存成功
req.session.test = 'value';
req.session.save((err) => {
if (err) console.error('保存失败:', err);
});
// 3. 检查存储连接
redisClient.ping((err, result) => {
console.log('Redis 连接:', err ? '失败' : '正常');
});
2. 跨域问题
javascript
// 前端需要设置 withCredentials
fetch('/api/data', {
credentials: 'include' // 包含 Cookie
});
// 后端需要设置 CORS
app.use(cors({
origin: 'https://frontend.com',
credentials: true, // 允许凭证
methods: ['GET', 'POST', 'PUT', 'DELETE']
}));
总结
Cookie-Session 机制的核心是:
Cookie 只存 ID:安全、小巧、自动传输
Session 存数据:完整、安全、服务器可控
通过 ID 关联:建立客户端和服务器的信任桥梁
配合安全策略:HttpOnly、Secure、SameSite 多重防护
这种机制在传统 Web 应用中表现优秀,特别是需要服务器完全控制会话、需要存储大量临时数据、或需要支持服务器主动登出的场景。但在分布式系统和前后端分离架构中,需要考虑 Session 共享和扩展性问题