首页 / Node.js 教程 / Cookie 与 Session

Node.js 教程

Cookie 与 Session

本教程共 76 篇 · 第 55 篇 · 更新于 2026-07-25 · 约 9 分钟阅读

Node.jsCookieSession状态管理认证

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 看着简单,但属性配错了就是安全漏洞。这几个最关键:

  • 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: falsesaveUninitialized: 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 一般上限 4KB,每个域名下的 Cookie 数量和总大小也有限制。所以千万别把大对象往 Cookie 里塞——这正是 Session 用「只存 ID」设计的原因。session ID 通常就二三十个字符,轻飘飘的,浏览器毫无压力。

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。两套机制各管一摊,互不打扰。