深蓝黑背景品牌封面:左侧大标题「Cookie 和 Session」,副标题「存哪、带什么、分布式下怎么办」;右侧原创装饰——左边一台浏览器窗口,兜里揣着一张写着 sessionID 的小票(cookie),箭头连向右边一台服务器,服务器身后是一排抽屉柜(session 存储),表示浏览器只带编号、数据存在服务端;左上标注「经典题解析 · 网络」,右上标注「Day 02 · 10-10」 图 1:本文封面,「浏览器只带小票、数据存在服务端」的协作模型。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。cookie 和 session 的区别是网络面试的基本盘,但多数人只答到「一个存客户端一个存服务端」就停了——面试官想听的完整版,还包括它们怎么协作、安全属性怎么配、分布式部署后怎么办。

想象面试现场:「说说 cookie 和 session 的区别。」

第一步,先讲为什么需要它们。

HTTP 本身是无状态的:服务器收到两个请求,不知道它们是不是同一个人发的。会话机制要解决的就是这件事——让服务器认得出「回头客」。cookie 是浏览器端的存储机制,session 是服务端的数据结构,两者不是对立关系,而是协作关系:最常见的组合就是 cookie 揣着 session 的编号。

第二步,协作流程跑一遍。

登录成功后,服务器在内存或存储里创建一份 session 数据(用户是谁、购物车有什么),生成一个随机编号 sessionID,通过响应头 Set-Cookie 发给浏览器。浏览器存下这张「小票」,之后对这个站点的每个请求自动带上 Cookie 头,服务器按编号取出对应数据。浏览器端只有一串无意义的编号,真实数据始终在服务端——这是 session 方案安全性的来源。

下面这个最小演示把流程跑给你看,用 Node.js 22.15.0 直接运行(node session-demo.js)即可:

'use strict';
const http = require('node:http');

// 最小会话演示:session 数据存服务器内存,cookie 只带 sessionID
const sessions = new Map();

const server = http.createServer((req, res) => {
  const cookie = req.headers.cookie || '';
  const match = cookie.match(/sessionID=(\w+)/);
  const sid = match ? match[1] : null;

  if (req.url === '/login') {
    const newSid = Math.random().toString(36).slice(2, 10);
    sessions.set(newSid, { user: 'xiaolin', visits: 1 });
    // HttpOnly:脚本读不到;Max-Age:10 分钟过期
    res.setHeader('Set-Cookie', `sessionID=${newSid}; HttpOnly; Path=/; Max-Age=600`);
    res.end(`登录成功,已下发 sessionID=${newSid}\n`);
    return;
  }

  const sess = sid && sessions.get(sid);
  if (sess) {
    sess.visits++;
    res.end(`欢迎回来 ${sess.user},这是你第 ${sess.visits} 次访问\n`);
  } else {
    res.end('没有有效会话,请先访问 /login\n');
  }
});

server.listen(0, '127.0.0.1', async () => {
  const base = `http://127.0.0.1:${server.address().port}`;
  const show = async (label, opts) => {
    const r = await fetch(base + opts.path, { headers: opts.headers || {}, redirect: 'manual' });
    const setCookie = r.headers.get('set-cookie');
    console.log(`${label} → ${(await r.text()).trim()}` + (setCookie ? `|响应头 Set-Cookie: ${setCookie}` : ''));
    return setCookie;
  };

  await show('请求 1:不带任何凭证访问', { path: '/' });
  const sc = await show('请求 2:登录', { path: '/login' });
  const sid = sc.match(/sessionID=\w+/)[0];
  await show('请求 3:带上 cookie 回访', { path: '/', headers: { cookie: sid } });
  await show('请求 4:换一个伪造的 sessionID', { path: '/', headers: { cookie: 'sessionID=fake1234' } });
  server.close();
});

实测输出如下(sessionID 每次运行随机):

请求 1:不带任何凭证访问 → 没有有效会话,请先访问 /login
请求 2:登录 → 登录成功,已下发 sessionID=fdvv0r7u|响应头 Set-Cookie: sessionID=fdvv0r7u; HttpOnly; Path=/; Max-Age=600
请求 3:带上 cookie 回访 → 欢迎回来 xiaolin,这是你第 2 次访问
请求 4:换一个伪造的 sessionID → 没有有效会话,请先访问 /login

最后一行值得停一下:伪造的编号在服务器查不到对应 session,直接被拒——sessionID 必须是够长够随机的字符串,让「猜编号」在计算上不可行。

第三步,对比维度逐项过。

存储位置:cookie 在浏览器,session 在服务器。容量:cookie 受浏览器限制,单个通常只有约 4KB,数量也有限;session 基本只受服务器资源约束。安全属性:cookie 有三个必须会讲的开关——HttpOnly 禁止 JavaScript 读取,挡住 XSS 偷凭证;Secure 只在 HTTPS 下发送,防明文窃听;SameSite 控制跨站请求是否携带,是防 CSRF 的一道闸门。生命周期:不带过期时间的 cookie 是会话 cookie,浏览器关闭即删;session 则由服务端的过期策略说了算。

第四步,分布式这个必考追问。

「多台服务器部署后,session 存哪?」本机内存的方案立刻失效:第一次请求落在 A 机登录成功,第二次被负载均衡分到 B 机,就「丢登录」了。两条主流出路:一是集中存储,把 session 搬进 Redis 这类所有实例都能访问的存储,服务器变成无状态,随便扩容;二是无状态 Token,把会话信息签名后直接放进凭证里(JWT 是这个思路的代表),服务器不存任何东西,验签即认人。前者简单可控、可随时吊销,后者免存储但吊销麻烦,选型看业务对「踢人下线」的要求。

流程加对比图:上部是会话建立与回访流程——登录成功后服务器创建 session 数据,通过 Set-Cookie 把 sessionID 发给浏览器,之后每个请求自动带上这张「小票」,服务器按编号取出对应的会话数据。中部两列对比 cookie 与 session:存储位置(浏览器对服务器)、容量(约 4KB 对基本不受限)、安全属性(HttpOnly 防脚本读取、Secure 只走 HTTPS、SameSite 防跨站携带)。下部是分布式问题:多实例部署时本机 session 会导致「换台机器就丢登录」,两条出路是把 session 搬进集中存储(如 Redis)或改用无状态 Token 自包含会话信息 图 2:会话流程、维度对比与分布式会话的两条出路。

易错点清单。

一,把 cookie 和 session 说成二选一的对立技术,讲不出协作流程。二,sessionID 用自增序号或可猜的格式,等于把账号拱手让人。三,cookie 不配 HttpOnly 和 Secure,凭证裸奔。四,上多实例部署才想起来 session 还在单机内存里。

回到面试现场,建议的答题节奏:先讲无状态问题与「编号小票」协作模型,再过存储位置、容量、安全属性的对比,最后主动把分布式会话的两条出路摆上桌面。AI 应用的多轮对话上下文、匿名额度控制全都建立在这套机制上,会话存哪里,直接决定你的服务能不能无状态扩容。

你的项目用的是集中 session 还是无状态 Token?踩过哪些坑?欢迎分享。