🕸️ 第二章 Web特有的架构
📄 一句话概括

会话是【柜子】,会话 ID 是【钥匙】——柜子在服务器那边,钥匙在浏览器这边,两者合起来才把无状态的 HTTP 变成一次有头有尾的交谈。

📝 本节和前面几节的关系

2-13 讲了 Cookie 怎么把凭证带来带去;这一条讲凭证的另一头:那串 ID 指向的东西存在哪、怎么用、什么时候该换。两条合起来,就是把无状态变成有记性的完整办法。它也是本章的收官。

一、一次交谈要有头有尾

会话指的是一次从头到尾的连续交谈。比如【登录 → 浏览商品 → 加购物车 → 下单 → 付款】,这一整串属于同一个会话。

麻烦在于 HTTP 无状态,每条请求都是新的。怎么把这几步串成一条线?

办法是给这条线起个名字——会话 ID。浏览器每次带着这个名字来,服务器就知道这几条请求说的是同一件事。

二、钥匙与柜子

会话 ID 是钥匙:一小段随机字符串,放在 Cookie 里来回带。会话是柜子:内容在服务器那边——你是谁、买了什么、走到哪一步了。

钥匙上什么都不写,所以钥匙被人看到也读不出信息;但钥匙被复制走,他就能开你的柜子——这就是会话劫持。

于是有了另一派做法:把信息直接写进钥匙里,这就是 JWT(JSON Web Token)。两种做法各有各的账。

三、动手 Demo:两条路的账,摊在十万次请求上

sid, jwt = 100, 850
print("每次请求多带:会话 %d 字节  vs  令牌 %d 字节" % (sid, jwt))
print("10 万次多传:%.1f MB  vs  %.1f MB"
      % (sid * 1e5 / 1048576.0, jwt * 1e5 / 1048576.0))
print("服务器开销:0.5 毫秒 x 10 万 = %.0f 秒  vs  0.05 毫秒 x 10 万 = %.0f 秒"
      % (0.5 * 1e5 / 1000.0, 0.05 * 1e5 / 1000.0))

实跑输出:

--------------------------------------------------------------------
D14  系列处理流程的管理:会话、会话 ID
--------------------------------------------------------------------
  会话 = 一次【从头到尾的连续交谈】。
  比如【登录 -> 浏览商品 -> 加购物车 -> 下单 -> 付款】,这是同一个会话。

  麻烦在于 HTTP 无状态,每条请求都是新的。怎么把这几步串成一条线?
  办法是给这条线起个名字——会话 ID。

  两者的分工,用一个比方最清楚:
    会话 ID 是【钥匙】:一小段随机字符串,放在 Cookie 里来回带;
    会话是【柜子】:内容存在服务器那边——你是谁、买了什么、到哪一步了。
    钥匙上什么都不写,所以钥匙被人看到也读不出信息;
    但钥匙被复制走,他就能开你的柜子——这就是会话劫持。

  于是有了另一派做法:把信息直接写进钥匙里,这就是 JWT。
  ----------------------------------------------------------------------------------------
  对比项          会话 ID(存服务器)             JWT(信息在令牌里)
  ----------------------------------------------------------------------------------------
  存在哪          服务器的存储里                  令牌自己身上
  每次请求        一次存储查询                    一次签名校验
  大小            约 32 字节                      约 300~800 字节
  能撤销吗        删掉记录立刻生效                难,得等它过期
  跨服务          要共享同一个存储                各服务拿公钥自己验
  ----------------------------------------------------------------------------------------

  把两条路的开销摊在 10 万次请求上看:
  ----------------------------------------------------------------------------------------
  项目                          会话 ID             JWT
  ----------------------------------------------------------------------------------------
  每次请求多带的字节            约 100              约 850
  10 万次多传的数据             约 9.5 MB           约 81.1 MB
  每次请求的服务器开销          约 0.5 毫秒         约 0.05 毫秒
  10 万次的服务器开销           约 50 秒            约 5 秒
  ----------------------------------------------------------------------------------------
  两笔账正好相反:JWT 省服务器、费带宽;会话省带宽、费服务器。

  但真正的分水岭不是这两个数字,是【能不能立刻撤销】:
    用户点了退出、账号被盗、权限被收回——
    会话这条路:删一行记录,下一秒所有人都不认这把钥匙;
    令牌这条路:要么等它自己过期(常见 15 分钟),
    要么再建一张【已撤销名单】——等于把刚省下的那份状态又加了回来。

  所以 2026 年最常见的做法是两条路各取一半:
    浏览器这边用会话 Cookie(HttpOnly + Secure + SameSite=Lax);
    服务器内部几个服务之间互调,用短期的令牌。

  最后是四条不该省的规矩:
  ----------------------------------------------------------------------------------------
  规矩                                为什么
  ----------------------------------------------------------------------------------------
  会话 ID 至少 128 位随机数           太短会被猜,猜到一次就等于登录成功
  登录成功后必须换一个新 ID           防会话固定攻击(攻击者预先埋一个 ID 给你)
  带上 HttpOnly 与 Secure             脚本读不到、明文拿不到
  设置过期时间(闲置 + 绝对)         让被遗忘的会话自己关掉
  ----------------------------------------------------------------------------------------

  一句话收尾:Cookie 是【怎么把钥匙带来带去】,
  会话是【这把钥匙能开哪个柜子】——两者合起来,
  才把无状态的 HTTP 变成了一次有头有尾的交谈。

读这个输出:两条路的账正好相反——会话省带宽、费服务器;令牌省服务器、费带宽。注意这两个数字都还不算大,真正让人做决定的不是它们,而是下面这件事:撤销。

四、真正的分水岭:能不能立刻撤销

用户点了退出、账号被盗、权限被收回——这三种情况都要求【立刻不认】。

会话这条路:删掉那一行记录,下一秒所有人都不认这把钥匙了。令牌这条路:要么等它自己过期(常见 15 分钟),要么再建一张【已撤销名单】——等于把刚省下的那份状态又加了回来。

所以 2026 年最常见的做法是两条路各取一半:浏览器这边用会话 Cookie(HttpOnly + Secure + SameSite),服务器内部几个服务之间互调时用短期的令牌。用户浏览器里从来没有令牌,令牌只在服务之间流动。

五、四条不该省的规矩

第一,会话 ID 至少要 128 位随机数——太短会被猜,而猜到一次就等于登录成功。

第二,登录成功后必须换一个新的会话 ID——这是防【会话固定攻击】:攻击者预先埋一个他知道的 ID 给你用,等你登录成功,那个 ID 就变成了你的钥匙。

第三,Cookie 要带上 HttpOnly 与 Secure——脚本读不到、明文拿不到。第四,设置过期时间(闲置多久自动失效 + 最长活多久),让被遗忘的会话自己关掉。

一句话收尾:Cookie 是【怎么把钥匙带来带去】,会话是【这把钥匙能开哪个柜子】——两者合起来,才把无状态的 HTTP 变成了一次有头有尾的交谈。这也是第二章从【无状态】一路讲到这里的原因。

30 秒实验

  1. 登录一个你需要账号的网站,然后按 F12 打开 Application 面板,在 Cookies 里找到那条会话。
  2. 看它的属性:HttpOnly 勾了吗?Secure 勾了吗?过期时间是【会话结束时】还是一个具体时间?
  3. 点退出登录,再回来看——那条 Cookie 还在不在?值变了吗?

常见误区

  1. 错误:会话就是 Cookie,两个词说的是一回事。——纠正:它们是两个层面。Cookie 是浏览器这一侧的传输机制,会话是服务器这一侧的存储。一个负责把钥匙带来带去,一个负责保管柜子里的东西——教材把它们分成相邻两条,正是为了拆开这一点。
  2. 错误:JWT 比会话先进,所以应该优先用它。——纠正:取舍不在新旧,在【要不要立刻撤销】。单体的、面向浏览器的应用,会话更简单也更安全;多服务、跨域、移动端,令牌更合适。2026 年最常见的做法是两者混用,各管一段。
  3. 错误:登录验证通过就完事了。——纠正:登录成功后必须换一个新的会话 ID。否则就给了【会话固定攻击】可乘之机——攻击者先把一个自己知道的 ID 塞给你,你登录之后,那条 ID 就自动升级成了你的登录凭证。这一步不做,前面所有密码校验都白费。
  4. 错误:会话 ID 是随机生成的,猜不到。——纠正:随机不等于够长。熵不够的随机数可以被暴力枚举,而猜中一次就等于登录成功一次。所以这里有一条明确的下限:至少 128 位。
自测三问(点开对答案) 1. 为什么说【会话是柜子、会话 ID 是钥匙】? —— 因为**信息存在服务器那边,浏览器手里只有一串没有含义的编号**。这个分工带来两个后果:钥匙被看到也读不出内容(安全),但钥匙被复制走就能开柜子(所以要做 HttpOnly、要能立刻撤销)。**理解了这一层,后面所有的安全措施都有地方挂。** 2. 为什么【能不能立刻撤销】比带宽和服务器开销更重要? —— 因为**带宽和开销是常态成本,撤销能力是事故成本**。用户点了退出、账号被盗,这些场景要求【下一秒就不认】。**常态成本可以优化,事故成本只能靠设计提前留好**——所以绝大多数面向浏览器的应用会选会话。 3. 第二章十四条,如果只记住一句话,记哪句? —— 记 **【HTTP 不记人,所以要在它外面加上一层记性】。** 这一章从 SoR 与 SoE 的分工讲起,经过 HTML、CSS、静态与动态、请求与响应、开发者工具、CGI、前后端分离、两边的脚本语言,最后落在 Cookie 与会话——**整条线都在回答同一个问题:一个天生不留记忆的协议,怎么让一连串操作看起来像一次完整的交谈。**

相关笔记