Cookie 与 Session
本教程共 76 篇 · 第 55 篇 · 更新于 2026-07-25 · 约 9 分钟阅读
55. Cookie 与 Session
本节目标:- Cookie 到底是什么,服务器怎么把它塞进浏览器、浏览器又怎么带回来 - Cookie 的几个安全属性(httpOnly、secure、sameSite)各自防什么 - Session 为什么比「把数据全塞进 Cookie」更靠谱 - 用
express-session在 Express 里落地会话管理
HTTP 是个「记性差」的协议。浏览器发一个请求过来,服务器处理完回个响应,这次交互就结束了。下一个请求再来,服务器压根不知道「你」是不是上一秒那个「你」。这种特性叫无状态(stateless)。
可现实里的网站得记住用户:登录状态、购物车、偏好设置……总不能每点一个按钮都重新输一遍账号密码。Cookie 和 Session 就是为了解决这个问题而生的两套配合机制。
Cookie:浏览器替你存的便签
Cookie 本质就是一小段文本。服务器通过响应头 Set-Cookie 告诉浏览器「存这个」,浏览器之后访问同域名时,会把它自动带在请求头 Cookie 里发回去。
HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; Path=/; HttpOnly
下次请求:
GET /dashboard HTTP/1.1
Cookie: sessionId=abc123
在 Express 里手写设置 Cookie 很简单:
import express from 'express';
const app = express();
app.get('/set', (req, res) => {
res.cookie('theme', 'dark', {
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天,毫秒
httpOnly: true,
sameSite: 'lax',
});
res.send('cookie 已写入');
});
app.get('/read', (req, res) => {
res.send(`你选的主题是: ${req.headers.cookie}`);
});
app.listen(3000);
Note直接读
req.headers.cookie拿到的是一整串字符串,得自己解析。Express 生态里有cookie-parser帮你解析成对象,但如果你用的是下面要讲的express-session,它自己会读 Cookie,不需要再挂cookie-parser。
三个保命的 Cookie 属性
Cookie 看着简单,但属性配错了就是安全漏洞。这几个最关键:
httpOnly: true—— 禁止前端 JavaScript 读这个 Cookie。没有它,一旦页面被 XSS 注入,攻击者一行document.cookie就能把你的会话 ID 偷走。secure: true—— 只在 HTTPS 连接下才发送。明文 HTTP 下 Cookie 会裸奔,容易被中间人截获。sameSite—— 控制跨站请求时是否携带。strict最严(完全不带跨站),lax是多数框架的默认(安全的 GET 跨站带,POST 表单不带),none则是显式允许跨站(必须配合secure)。这个属性是防 CSRF 的第一道闸。
res.cookie('sessionId', id, {
httpOnly: true,
secure: true, // 生产环境必须,前提是站点走 HTTPS
sameSite: 'lax',
maxAge: 1000 * 60 * 30,
});
Warning
secure: true后,如果你在本地用http://localhost测试,Cookie 会设不进去。开发阶段可以先关掉,或者给express-session配一个根据NODE_ENV决定是否开启的逻辑(下面有示例)。
Session:服务器端的「档案袋」
Cookie 容量很小(通常 4KB 上限),而且存在浏览器端,用户能看能改。你要是把自己是不是管理员、用户 ID 这些敏感信息直接塞 Cookie,等于把家门钥匙挂在门把手上。
Session 的思路是:服务器在自己这边维护一份用户状态(叫 session store),只把一个无意义的随机 ID(session ID)通过 Cookie 发给浏览器。浏览器每次请求带上这个 ID,服务器查自己的档案袋,就知道你是谁了。
浏览器 Cookie: sessionId=abc123 (只有一把钥匙)
服务器内存/Redis: { abc123: { userId: 7, role: 'admin' } } (真正的档案在保险柜)
这样即使 Cookie 被偷,攻击者拿到的也只是一串随机 ID;而且你可以随时在服务端把某个 session 作废(比如用户点了退出登录)。
express-session 落地
express-session 是 Express 官方维护的会话中间件,当前稳定版是 1.18.x。先装:
npm install express-session
最小可用版本:
import express from 'express';
import session from 'express-session';
const app = express();
app.use(session({
secret: '换成一个够长的随机字符串',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 30 },
}));
挂上这个中间件之后,每个请求对象上就有了一个 req.session,你可以往里塞任意可序列化的数据:
// 登录成功后
app.post('/login', (req, res) => {
// 假设前面已经校验过账号密码
req.session.userId = 7;
req.session.role = 'admin';
res.send('登录成功');
});
// 需要登录才能看的页面
app.get('/dashboard', (req, res) => {
if (!req.session.userId) {
return res.status(401).send('请先登录');
}
res.send(`欢迎用户 ${req.session.userId}`);
});
// 退出登录
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) return res.status(500).send('退出失败');
res.send('已退出');
});
});
secret 是给 session ID 做签名用的,用来防止客户端伪造。它必须够长够随机,并且从环境变量读,千万别硬编码在代码里提交到仓库。
Tip
resave: false和saveUninitialized: false是现在推荐的组合:saveUninitialized: false意味着「没往 session 里写东西就不发 Cookie」,能减少无谓的 Cookie 下发。
生产环境要换掉默认存储
express-session 默认用 MemoryStore,它明确写着不适合生产:进程一重启 session 全没、多进程/多实例之间不共享、还会慢慢漏内存。
生产环境要把存储换成 Redis、Memcached 这类外部存储。以 Redis 为例(需要先 npm install connect-redis redis):
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient();
redisClient.connect().catch(console.error);
const store = new RedisStore({ client: redisClient });
app.use(session({
secret: process.env.SESSION_SECRET,
store,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 1000 * 60 * 30,
},
}));
换成 Redis 之后,不管你起几个 Node 实例(比如用第 34 章的 cluster,或者跑多个容器),用户的 session 都落在同一份存储里,谁都能认出他。
代理后面的那些坑
生产环境你几乎一定会把 Node 放在 Nginx 这类反向代理后面,再用 HTTPS 对外。这时候浏览器是跟 Nginx 走 HTTPS,Nginx 再用 HTTP 把请求转发给 Node。结果就是:Node 看自己收到的是 HTTP 请求,觉得 secure: true 的 Cookie 不能设。
解决办法是告诉 Express「我前面有可信代理」,让它按 X-Forwarded-Proto 头来判断真实协议:
app.set('trust proxy', 1); // 信任第一层代理
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true, // 现在能正常设了,因为 Express 知道外面是 HTTPS
sameSite: 'lax',
},
}));
另外还有个常被忽略的攻击叫会话固定(session fixation):攻击者先自己登录拿到一个 session ID,诱骗受害者用这个 ID 登录,之后攻击者就能借这个已认证的会话操作。防御方法是「登录成功后重新生成 session ID」,避免沿用登录前的那个。在 express-session 里登录后调一下 req.session.regenerate() 即可。
Cookie 的容量和边界
Cookie 不是随便塞的:单个 Cookie 一般上限 4KB,每个域名下的 Cookie 数量和总大小也有限制。所以千万别把大对象往 Cookie 里塞——这正是 Session 用「只存 ID」设计的原因。session ID 通常就二三十个字符,轻飘飘的,浏览器毫无压力。
Cookie 和 Session 的局限
Session 方案稳,但也有代价:状态存在服务端,意味着你必须维护一个存储(Redis 之类),而且水平扩展时要保证存储共享。另外每次请求都要去存储里查一次,有额外的延迟。
正因为这些,现代 API(尤其是移动端、微服务、多端共用)越来越爱用另一种思路——不存服务端状态,把「你是谁」直接编码进一个签名过的令牌里发给前端,这就是下一章要讲的 JWT。
不管走哪条路,Cookie 的安全属性(httpOnly / secure / sameSite)都值得你花心思配好,它们是挡在会话劫持和 CSRF 前面最便宜也最有效的一道墙。
Session 和 JWT 到底怎么选
前面已经埋了伏笔,这里把账算清楚。Session 的优点是「服务端说了算」:用户干了坏事,你直接从 Redis 里把他的 session 删了,他立刻掉线,吊销即时生效;缺点是要维护一个共享存储,水平扩容时所有实例都得连同一份 Redis,架构耦合在状态上。
JWT 反过来:服务端零状态,哪个实例收到请求都能独立验签,特别适合微服务、移动端、还有需要把令牌发给第三方的情况;缺点正是「太自由」——令牌一旦签发,在过期前你很难单方面让它提前失效,除非你再去维护一份吊销名单,这又把自己绕回了 Session 的思路。
所以别听谁说「JWT 更现代就全用 JWT」。传统的、状态集中的 Web 应用,Session 其实更省心也更可控;无状态、多端、跨服务的 API,JWT 才显出价值。选型看的是你的架构痛点,不是流行度。
Tip一个很常见的混合做法:用 Session 管网页登录状态(Cookie 里只放 session ID),同时给需要调用的 API 另发 JWT。两套机制各管一摊,互不打扰。