第 9 章 无状态与状态:Cookie、Session 与 Token
HTTP 是”无状态”的,但你的业务是”有状态”的。这中间的裂缝,全靠一个 1994 年的”临时方案”在填——它叫 Cookie,它用了 30 年,至今还在制造安全事故。
9.0 引子:一刷新就掉线
你:墨叔,用户投诉,登录成功了,一刷新页面,就掉线了,又要重新登录。
老张:还有更怪的。加了台机器做负载均衡,结果一半的用户登录态丢了,不停被踢回登录页。
你:登录态存哪了?我存的服务端内存里……哦。
墨叔:看到了吗?你掉进了一个经典的坑。而这个坑的根源,是一个你需要先想清楚的问题:
HTTP 是”无状态”的。那你的”登录状态”,到底存在哪儿?
9.1 先把问题问对:无状态是缺陷,还是设计选择?
墨叔:先纠正一个常见的误解——“无状态”不是 HTTP 的缺陷,是它刻意的、伟大的设计选择。
你:无状态还能是优点?
墨叔:你想,如果 HTTP 是”有状态”的——服务器要记住每个客户端的”上次请求是什么”——会怎样?
后果一:无法水平扩展。 状态存在某台服务器上,那这个客户端的下次请求必须路由回同一台服务器(这叫”会话保持/粘性”)。一旦这台服务器挂了,所有挂在这台机器上的会话全丢。而且加机器时,状态怎么迁移?——有状态 = 扩容噩梦。
后果二:协议复杂化。 服务器要维护”连接 → 会话”的映射,要处理”会话什么时候过期”、要处理”客户端重启后状态怎么恢复”……协议复杂度爆炸。
后果三:缓存失效。 如果响应依赖”服务器记得的上下文”,那同一个 URL 对不同请求返回不同内容,缓存就全乱了(第 8 章的 Vary 问题)。
所以 Fielding 把”无状态”列为 REST 的核心约束:
服务器不保存客户端上下文。每个请求都”自包含”——携带了服务器处理它所需的全部信息。
你:那代价呢?代价就是……业务需要状态时,得自己想办法?
墨叔:对。无状态的代价是:所有”状态”都得由客户端自己带着(每次请求重新提交),或者存在一个”第三方的、可共享的”地方(数据库/Redis)。
而”客户端自己带着状态”这件事,就是 Cookie 诞生的原因。
9.2 Cookie 的本质:一个用了 30 年的”临时 hack”
墨叔:Cookie 的出身其实挺卑微的。
1994 年,网景(Netscape)的程序员需要让”购物车”能在无状态的 HTTP 上工作。方案是:服务器把一个”小标签”发给浏览器,浏览器下次请求时自动把这个标签带回来。
# 服务器响应
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; Path=/; HttpOnly
^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^
名字=值 作用域 安全属性
# 浏览器下次请求,自动带上
GET /cart HTTP/1.1
Cookie: session_id=abc123你:所以 Cookie 是”服务器发的、浏览器自动回传的、一小段状态”?
墨叔:精确。它本质是——
HTTP 的”状态外挂”:一个本来无状态的协议,通过”服务器种 cookie → 浏览器存 → 浏览器自动回传”这套机制,硬是模拟出了”状态”。
你:你说它”用了 30 年至今还在制造安全事故”,为什么?
墨叔:因为它有两个天生缺陷,是设计时根本没考虑到的:
- 它是个”外挂”,不是协议的核心。所以它游离在各种安全机制的边缘,需要一堆额外的属性(HttpOnly、Secure、SameSite)来补漏洞。
- 它是”自动回传”的。浏览器会自动把 Cookie 带回它所属的域。这个”自动”,正是 CSRF 攻击的根源(后面讲)。
但讽刺的是:正是因为它是”外挂”、是”后加的”,它才能活 30 年——如果它是协议核心的一部分,早就随着协议升级被重构掉了。
9.3 Set-Cookie 的属性:一层一层防线
墨叔:Cookie 的安全,全靠 Set-Cookie 后面的那串属性。我们一层层看。
Set-Cookie: session=abc; Expires=...; Max-Age=3600; Domain=example.com; Path=/app; Secure; HttpOnly; SameSite=Lax作用域三件套:Domain / Path / Secure 的一部分
Domain:这个 Cookie 能被带回哪些域名。Domain=example.com意味着example.com及其子域名(a.example.com、b.example.com)都能拿到它。注意:不写 Domain,则 Cookie 只属于精确的当前主机(不含子域)。Path:Cookie 只在匹配这个路径前缀的请求里回传。Path=/app只在/app/xxx的请求里带。Secure:只允许在 HTTPS 连接里传输这个 Cookie。没有它,Cookie 会在明文 HTTP 里裸奔,被中间人截获。
防线一:HttpOnly —— 让 JS 读不到
你:引子里我问过,HttpOnly 的 Cookie 为什么 JS 读不到?
墨叔:这是故意的。HttpOnly 的意思是:这个 Cookie 只能被浏览器在 HTTP 请求里”自动回传”,不能被 JavaScript 通过 document.cookie 读取。
为什么需要这个?为了防 XSS(跨站脚本攻击):
- 如果攻击者通过 XSS 漏洞在你的页面里注入了一段恶意 JS。
- 这段 JS 想偷你的登录态,它会执行
document.cookie,把 Cookie 读出来发到攻击者服务器。 - 但如果你的会话 Cookie 标了
HttpOnly,恶意 JS 就读不到它(浏览器拒绝暴露)。
所以你问”为什么 JS 读不到”——答案是”这是特性,是为了让偷 Cookie 的攻击者读不到”。 很多人第一次发现 document.cookie 读不到自己的会话 Cookie,以为是 bug,其实那正是它保护你的方式。
防线二:SameSite —— 防 CSRF
墨叔:SameSite 是近几年最重要、也最容易被误解的 Cookie 属性,它防的是 CSRF(跨站请求伪造)。
先讲 CSRF 是什么:
场景:你登录了银行网站 bank.com,会话 Cookie 存在浏览器里。然后你(在同一个浏览器里)访问了一个恶意网站 evil.com。evil.com 的页面上有一个隐藏的表单:
<img src="https://bank.com/transfer?to=attacker&amount=10000">浏览器加载这个 <img> 时,会自动向 bank.com 发请求,并且——自动带上你的会话 Cookie(因为目标域是 bank.com)。银行服务器看到”有效 Cookie + 转账请求”,以为是你本人操作,把钱转走了。
你:……所以 CSRF 是”借刀杀人”——攻击者借浏览器”自动带 Cookie”的特性,伪造了一个”你发出的请求”。
墨叔:精确。CSRF 的本质是:
浏览器无法区分”这是用户主动点的请求”还是”第三方页面诱导发出的请求”。它一视同仁地带上 Cookie。
SameSite 就是来解决这个的,它有三个值:
| 值 | 行为 | 效果 |
|---|---|---|
SameSite=Strict | Cookie 只在同站请求里发送,任何跨站请求都不带 | 最安全,但体验差(从别的网站点链接跳过来,都不带 Cookie,导致”已登录却显示未登录”) |
SameSite=Lax | Cookie 只在顶级导航(点链接跳转)时发送,跨站的子资源请求(img、iframe、AJAX)不带 | 平衡:点链接能保持登录,但 CSRF 的 img/表单攻击被拦 |
SameSite=None | Cookie 无条件发送(必须同时配 Secure) | 给第三方嵌入场景用(如嵌入的支付组件) |
你:所以现在浏览器的默认值是 Lax?
墨叔:对。2020 年起,Chrome 等主流浏览器把未设置 SameSite 的 Cookie 默认当作 Lax 处理。这是一个重要的安全升级——它让”没显式设置 SameSite”的老 Cookie,默认获得了 Lax 级别的 CSRF 防护。
9.4 Session 和 Token:状态到底存哪
墨叔:现在回到引子里”加机器导致掉线”的问题。核心是:“状态”存在哪里。
方案一:服务端 Session(状态存服务器)
用户登录 → 服务器生成 session_id → 存到【服务器内存/Redis】→ 把 session_id 种成 Cookie
下次请求 → 浏览器带 session_id → 服务器查 session_id → 找到用户状态
你:这就是我踩坑的方案。加机器为什么掉线?
墨叔:因为 session 存在单台服务器的内存里。加了负载均衡后:
- 用户第一次请求被路由到服务器 A,session 存在 A 的内存。
- 用户刷新,第二次请求被路由到服务器 B。
- B 的内存里没有这个 session_id 对应的状态 → B 认为用户没登录 → 踢回登录页。
这就是”有状态服务器”的水平扩展灾难。 解法:
- 粘性会话(sticky session):让同一个用户始终路由到同一台服务器。但服务器挂了 session 就丢,且不均衡。
- 集中存储:把 session 存到 Redis(所有服务器共享)。这是标准解法——服务器变回”无状态”,状态进了”共享存储”。
你:所以”无状态”不是”没有状态”,是”状态不放在服务器本地,放在共享的存储里”?
墨叔:对!这是最关键的理解:
“无状态”的本质,不是”消灭状态”,而是”把状态从’服务器本地’挪到’可共享、可水平扩展的地方’(客户端 Cookie / Redis / 数据库)”。 服务器本身保持无状态,才能随时扩容、随时重启。
方案二:Token(状态存客户端,或存共享存储但”自包含”)
Token 方案(如 JWT):服务器不存 session,而是把用户信息签名后直接发给客户端,客户端每次请求带上。
登录 → 服务器签发一个 token(里面编码了 user_id、过期时间,用密钥签名)
下次请求 → 客户端带 Authorization: Bearer <token>
服务器 → 验签(不查数据库)→ 从 token 里解码出 user_id
你:JWT 的好处是”服务器不用查 session”?
墨叔:对,但代价也很明显:
- 无法主动失效:session 可以”删掉 session_id”来踢人下线,但 JWT 是”自包含”的,服务器删不掉已经发出去的 token(除非引入”黑名单”,那就又变回查存储了)。
- token 泄露的风险:JWT 一旦泄露,攻击者可以冒充到过期为止(而 session 可以随时失效)。
所以,Session 和 Token 不是”谁更好”,是”不同的权衡”:
| Session(存服务器) | Token(存客户端) | |
|---|---|---|
| 状态在哪 | 服务器(Redis) | 客户端(token 本身) |
| 能否主动失效 | 能(删 session) | 难(需黑名单) |
| 服务器压力 | 每次请求查 Redis | 每次请求验签(不查库) |
| 扩展性 | 需要共享存储 | 天然无状态 |
| 适合场景 | 传统 Web 应用、需要强控制的会话 | 移动端、微服务间、API |
墨叔:真正的高手,不会说”JWT 吊打 session”,而是知道**“你在用’状态放哪’这个决策,换什么好处、付什么代价”**。
9.5 Cookie 的隐形坑:作用域、大小、以及”端口不隔离”
墨叔:Cookie 有几个”你以为懂,其实会翻车”的坑。
坑一:Cookie 不区分端口
这是个大坑:Cookie 的作用域由 Domain + Path 决定,但不包含端口。
也就是说:localhost:3000 和 localhost:8080 是两个不同的服务,但它们的 Cookie 是共享的(因为 Domain 都是 localhost,Path 都是 /)。
你:所以两个服务之间会互相污染 Cookie?
墨叔:对。常见事故:前端开发时,localhost:3000(前端 dev server)和 localhost:8080(后端)共享 Cookie,一个服务的 Cookie 覆盖了另一个的,导致各种诡异 bug。Cookie 的隔离边界是”域名”,不是”域名+端口”。
坑二:Cookie 大小和数量
Cookie 有实际限制:单个 Cookie 约 4KB,每个域名约 20-50 个 Cookie(浏览器实现各异)。超过了,浏览器会静默丢弃(不报错!)。
你:静默丢弃?那塞太多 Cookie 就悄悄丢了?
墨叔:对。所以”把一堆东西塞 Cookie 里”是危险的——你可能某天发现某个 Cookie 悄悄没了,还不知道为什么。Cookie 应该只存”最小必要的会话标识”,不要把业务数据塞进去。
坑三:Cookie 膨胀的传输成本
第 5 章提过:Cookie 会在每个请求里自动带上。一个 2KB 的 Cookie,一天几百万请求,就是几个 GB 的无效传输,而且拖慢每个请求(请求头变大)。这是”无状态协议”的隐形成本。
9.6 设计者视角:为什么”无状态”值得付出这些代价
墨叔:最后问一个收敛性的问题。你看到 Cookie/Session/Token 这一堆补丁,可能会想:“既然业务天生有状态,当初干嘛不直接把 HTTP 设计成有状态的?”
你:对啊,省得搞这一堆。
墨叔:这个问题的答案,是理解整个 HTTP 架构的钥匙:
“无状态”是一个”全局最优”的选择,即使它给”局部”(单个应用)带来了麻烦。
无状态换来了三样东西,每一样都是今天互联网的基石:
- 水平扩展:服务器可以随便加、随便重启,不用考虑”状态迁移”。
- 故障恢复:一台服务器挂了,请求切到另一台,无感知(因为没有状态要恢复)。
- 缓存和 CDN:响应不依赖”服务器记得的上下文”,才能被任意中间节点缓存(第 8 章)。
而”有状态”的成本,是这三个好处全没了。
你:所以是”协议层面选无状态,应用层面自己管状态”?
墨叔:对,这叫**“把复杂性下放”**:
HTTP 在协议层做了”简单但可扩展”的选择(无状态),把”状态管理”这个复杂性,留给了应用层自己去处理。 应用层可以用 Cookie、Session、Token、Redis……想怎么管就怎么管。
这个”下放”是极其聪明的:因为不同的应用,对”状态”的需求千差万别(有的要强会话控制,有的只要一个 token)。协议如果硬性规定”怎么管状态”,反而会束缚所有应用。让应用自己选,协议保持简单,正是 HTTP 能服务”从静态网页到银行系统”所有场景的原因。
9.7 Go 落地:一个安全的会话 Cookie
墨叔:看一个”安全地种会话 Cookie”的 Go 代码:
func setSessionCookie(w http.ResponseWriter, sessionID string) {
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
Domain: "", // 不设 = 只属于当前主机
MaxAge: 86400, // 1 天
HttpOnly: true, // JS 读不到,防 XSS
Secure: true, // 只在 HTTPS 传输
SameSite: http.SameSiteLaxMode, // 防 CSRF
})
}你:这几个属性,正好对应了 9.3 节讲的防线。
墨叔:对。记住这个”安全 Cookie 四件套”:HttpOnly(防 XSS)+ Secure(防窃听)+ SameSite=Lax(防 CSRF)+ 合理的 MaxAge。
一个额外的坑:Go 里如果你用 http.Cookie{MaxAge: 0} 或负数,会删除 Cookie(这是 Go 的 API 约定)。很多人想”不过期”就设 MaxAge: 0,结果把 Cookie 删了。
9.8 排查清单:登录态/会话类问题
第 1 步:状态到底存哪了?
→ 服务器内存?(加机器就掉线)→ 迁到 Redis
第 2 步:Cookie 种对了吗?
→ Domain/Path 作用域对吗?跨域带得上吗?
→ Secure 了吗?(HTTPS 页面里非 Secure 的 Cookie 会被拒)
第 3 步:SameSite 设置了吗?
→ 跨站请求带不上 Cookie?先查 SameSite
第 4 步:被 HttpOnly 拦了吗?
→ 前端读不到 Cookie?先确认是不是 HttpOnly(这是特性)
第 5 步:Cookie 超限被静默丢弃了吗?
→ Cookie 太多/太大?精简
思考题
第 1 题(概念)
“HTTP 是无状态的”这句话,精确的含义是什么?请辨析:“无状态”是指”服务器不保存客户端上下文”,还是指”业务没有状态”? 这两者的区别为什么重要?
第 2 题(概念)
HttpOnly、Secure、SameSite 三个 Cookie 属性,分别防御什么攻击?请各用一句话说清它们的工作原理。
第 3 题(设计)
你要设计一个”登录 + 记住我 7 天”的功能。要求:
- 关闭浏览器再打开,仍然保持登录(持久登录)
- 用户能主动退出登录
- 即使 Cookie 被偷,攻击者也难以长期冒用
请设计:用什么机制(session 还是 token)?Cookie 属性怎么配?“记住我”的 token 和”会话 token”要不要分开?
第 4 题(生活类比)
墨叔:游乐园的”手环”(腕带),和 Cookie,有什么相似之处?
提示:游乐园进门时给你戴一个手环,你在园内玩各个项目时,工作人员一看你的手环就知道”你可以玩”(不用每次都查你的身份)。想想:
- 手环对应 Cookie 的什么?
- 手环上写了”有效期到今晚闭园”对应什么属性?
- 手环离开游乐园就失效对应什么?(提示:作用域)
- 如果手环被偷了,别人能戴吗?这暴露了 Cookie 的什么天生缺陷?
第 5 题(更深一层的发问)
“无状态”是”把复杂性下放到应用层”(9.6 节)。但”下放”是有代价的——每个应用都要自己重新发明一遍”状态管理”,而且发明得五花八门、bug 百出(Cookie 的安全事故、JWT 的失效难题、session 的存储问题)。
那么:
- 如果当初 HTTP 选择了”有状态”(协议内置会话管理),今天的世界会是什么样?举一个具体场景说明它带来的好处和坏处。
- 这个问题的本质,是软件设计里的一个经典权衡——“把复杂度放在哪一层”。请结合”TCP 提供可靠传输(应用层不用管丢包)“这个例子,说说你对”复杂度应该放在哪一层”的理解。
参考答案
第 1 题答案
精确含义:HTTP 无状态,指的是”服务器不在协议层保存任何关于客户端的历史上下文”——每个请求都被当作独立的、自包含的来处理,服务器不记得”这个客户端上次发过什么”。
它不是指”业务没有状态”。业务当然有状态(登录态、购物车、进度),只是这些状态不是由协议来维护的,而是由应用层(Cookie/Session/Token/Redis)来维护。
这个区分为什么重要:
- 如果误解成”业务没有状态”,你就会困惑:“那登录是怎么保持的?” 进而觉得”无状态是个谎言”。
- 理解成”协议不维护状态,状态由应用层维护”,你才能理解为什么会有 Cookie/Session 这一整套东西——它们不是”补丁”,而是”无状态协议”这个设计选择的必然配套。
一句话:无状态,是”协议层”的无状态;有状态,是”应用层”的有状态。两者不矛盾,前者恰恰是后者的前提。
第 2 题答案
| 属性 | 防御 | 工作原理 |
|---|---|---|
HttpOnly | XSS(跨站脚本) | 禁止 JS 通过 document.cookie 读取。即使攻击者注入恶意 JS,也偷不到这个 Cookie |
Secure | 窃听/中间人 | 只允许 Cookie 在 HTTPS 加密连接里传输,明文 HTTP 不发送,防止被网络窃听者截获 |
SameSite | CSRF(跨站请求伪造) | 限制 Cookie 只在”同站”请求里发送,跨站请求(第三方页面的 img/form/AJAX)不携带,从而让攻击者无法”借用”你的 Cookie 伪造请求 |
补充记忆口诀:HttpOnly 挡”偷”(XSS 偷 cookie),Secure 挡”听”(窃听),SameSite 挡”借”(CSRF 借用)。
第 3 题答案
机制选择:用 Token(而不是纯 session),因为”记住我 7 天”意味着状态要跨浏览器重启、跨服务器,token 自包含、天然无状态,更适合持久登录。
“会话 token”和”记住我 token”必须分开——这是本题的关键点:
| 会话 token | 记住我 token | |
|---|---|---|
| 生命周期 | 短(关闭浏览器就失效) | 长(7 天) |
| 存储位置 | 内存(或不落盘) | 落盘(持久) |
| 权限 | 完整权限 | 可限定为”仅用于续期,不能直接访问敏感操作” |
| 泄露后果 | 小(短命) | 大(7 天) |
为什么要分开:因为”长命 token”被偷的风险远大于”短命 token”。如果两者合一,一旦”记住我”的 Cookie 被偷,攻击者就拿到了7 天的完整权限。分开后,可以用”短期会话 token 干活 + 长期记住我 token 只负责续期”,降低风险。
Cookie 属性配置:
# 会话 token(短期)
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Max-Age=86400
# 记住我 token(长期)
Set-Cookie: remember=...; HttpOnly; Secure; SameSite=Lax; Max-Age=604800; Path=/auth/renew
防”Cookie 被偷后被长期冒用”的三个手段:
- “记住我 token”用”随机不透明 token”(存服务器/Redis,可吊销),而不是自包含 JWT——因为 JWT 无法主动失效,被偷了就只能等过期。
- 轮换(rotation):每次用”记住我 token”续期时,换发一个新的 token,旧 token 作废。这样即使 token 被偷,一旦攻击者和受害者”同时使用”,就有一方会触发”旧 token 已作废”,暴露被盗。
- 绑定设备指纹:token 和”浏览器指纹/IP”绑定,异地登录触发重新验证。
第 4 题答案
| 游乐园手环 | Cookie |
|---|---|
| 手环本身 | Cookie(服务器发的”小标签”) |
| “有效期到今晚闭园” | Max-Age / Expires |
| ”离开游乐园就失效” | Cookie 的作用域(Domain/Path)——只在”所属域”内回传 |
| ”工作人员一看手环就知道你能玩” | 浏览器自动回传 Cookie,服务器一看 Cookie 就识别会话 |
“手环被偷了,别人能戴吗?“——能。
这正是 Cookie 最根本的天生缺陷:Cookie 是”凭物不凭人”的。它只验证”你有没有这个手环/Cookie”,不验证”你是不是本人”。
- 你把手环摘下来借给朋友,朋友也能进园玩(因为工作人员只看手环)。
- 你的 Cookie 被偷(XSS、中间人、物理访问),攻击者拿着它就能冒充你(因为服务器只看 Cookie)。
这引出一个重要结论:
Cookie 是”持有即身份”的机制,它天然无法区分”合法持有”和”非法获取”。 所以 Cookie 的安全,本质上是在做”让 Cookie 更难被偷”(HttpOnly、Secure)和”让偷来的 Cookie 更难被用”(短有效期、SameSite、轮换、绑定指纹)。它没有”验证持有人”的能力,只能”降低被偷的概率”和”降低被偷后的损失”。
第 5 题答案
1. 如果 HTTP 内置”有状态”会怎样?
一个具体场景:假设 HTTP 协议内置了”会话”——服务器自动为每个客户端维护一个会话,客户端不需要自己带任何标识。
好处:
- 应用层不用自己搞 Cookie/Session 了,登录态”开箱即用”。
- 统一、规范,不会有”各家发明各家的、bug 百出”的问题。
坏处(这才是致命的):
- 服务器变重:每台服务器都要维护海量会话状态,内存爆炸。
- 无法水平扩展:会话绑死在单台机器上,加机器、重启、故障转移全变成灾难。
- 缓存全废:响应依赖”服务器的会话上下文”,同一个 URL 对每个人返回不同内容,CDN 缓存失效(第 8 章的 Vary 灾难)。
- 协议复杂化:要定义”会话怎么建立、怎么过期、怎么序列化、怎么在代理间传递”……协议膨胀。
结果:HTTP 会变成一个”又重又复杂、无法扩展、无法缓存”的协议,根本不可能支撑今天这个规模的互联网。
2. “复杂度放在哪一层”的理解:
这是软件设计的核心哲学问题。答案是:
复杂度无法被消灭,只能被”移动”。设计者的智慧,在于把它放在”最合适的那一层”。
“合适”的判断标准是:哪一层放这个复杂度,能换来最大的全局收益。
- TCP 把”可靠传输”的复杂度放在传输层:代价是”每个 TCP 实现都很复杂”,收益是”所有上层应用(HTTP、FTP、SSH…)都不用自己处理丢包、重传、乱序”。一个复杂度,服务了千千万万个应用——这是”放对层”的典范。
- HTTP 把”状态管理”的复杂度下放到应用层:代价是”每个应用要自己搞一套”,收益是”协议保持简单、可扩展、可缓存、无状态服务器可水平扩展”。宁可让一万个应用各自付出一点代价,也不让协议背上一个会拖垮全体的包袱。
判断”复杂度放哪层”的原则:
如果某个复杂度是”所有上层都需要的、且可以在底层一次性解决的”,就下沉到底层(像 TCP 的可靠性)。如果某个复杂度是”各上层需求千差万别、无法统一的”,就上浮到应用层(像 HTTP 的状态)。
这就是为什么:TCP 管”传输的可靠性”(普适、可统一),HTTP 不管”业务的状态”(各异、不可统一)。好的分层,是让每层管它”最擅长、最普适”的那部分复杂度。
一页纸小结
无状态是设计选择,不是缺陷:它换来水平扩展、故障恢复、可缓存。代价是”状态由应用层自己管”。
Cookie 的本质:1994 年的”临时 hack”,服务器种标签、浏览器自动回传,给无状态协议”外挂”了状态。
Cookie 三防线:HttpOnly 挡 XSS(偷)、Secure 挡窃听(听)、SameSite 挡 CSRF(借)。2020 年起 SameSite 默认 Lax。
Session vs Token:不是谁更好,是”状态放哪”的权衡。Session 存服务器(可主动失效,需共享存储);Token 存客户端(无状态,难失效)。
“无状态”的真义:不是消灭状态,是把状态从”服务器本地”挪到”可共享、可扩展”的地方。
Cookie 隐形坑:不区分端口、4KB/数量限制、膨胀拖慢请求。
核心哲学:复杂度无法消灭,只能移动。TCP 把可靠性下沉(普适),HTTP 把状态上浮(各异)。放对层 = 用最小代价换取最大全局收益。
下一章:状态管明白了,但还有一个你天天写、却总搞混的东西——“同一个资源,怎么给它不同的形态?” 移动端要 JSON、网页要 HTML、还要 gzip 压缩、还要断点续传。这背后是 HTTP 的一整套”协商”机制。我要问你一个高频混淆点:Content-Encoding 和 Transfer-Encoding,到底有什么区别?