📘 第二章 简单的HTTP协议
📄 一句话概括

Cookie 是服务端发给客户端、客户端每次自动带回的一小段文本。 它在无状态协议外面补上「你是谁」,代价是每个请求都多带几十字节。

📝 本节和前面几节的关系

2.3 节说 HTTP 无状态、认不出人。 这一节给出协议层面唯一的补丁:Set-Cookie 与 Cookie 两个首部。第八章讲认证时还会再回到它。

一、一来一回两个首部

服务端在响应里加 Set-Cookie: SID=s01; Path=/; HttpOnly,客户端记住; 之后每次请求都自动加 Cookie: SID=s01。 注意是「每次」——包括图片、CSS、JS 的请求,只要同域同路径。 这就是 Cookie 的固定开销:一个 20 字节的 Cookie,一个页面 100 个请求就是 2 KB。 客户端不需要任何代码就会这么做,这是它比「手写 Token 首部」方便的地方, 也是它容易被滥用(追踪)的原因。

二、三个属性决定它活多久、谁能看

属性 作用 不加的后果
Expires/Max-Age 活多久 关浏览器就没(会话 Cookie)
Domain/Path 发给哪些地址 只回发给设置它的域
Secure 只走 HTTPS 明文链路也会带出去
HttpOnly JS 读不到 XSS 能直接偷走
SameSite 跨站要不要带 默认宽松,CSRF 风险
SameSite 要特别说明:书里没有。它 2016 年才进入标准,2020 年后主流浏览器把默认值改成 Lax。
这是这本书出版后变化最大的一处,读到 Cookie 章节必须补上。
HttpOnly 与 Secure 是防 XSS 偷 Cookie 的两道墙,但都只是「降低损失」,
不能替代输入过滤(11.2.1)。

三、实测:不带 Cookie 就是新客人

Demo 里同一个客户端先不带 Cookie 请求,服务端发一个 SID=s01;后两次带上,服务端就认出来了。 另一条连接(同一台机器、同一个进程)不带 Cookie,直接又被当成新客人,拿到 s02。 身份完全由那个字符串决定,跟机器、IP、连接都无关。 这也解释了 Session 劫持(11.4.1):拿到那个字符串,就等于拿到了那个人。 所以它必须走 HTTPS,必须 HttpOnly,最好还要绑定一些辅助特征。

四、动手 Demo:发一个 Cookie,看服务端怎么认人

服务端用 sessions 字典记会话。请求没带 Cookie 就发一个新的,带了就按那个值认人并计数。

def resp(raw, st):
    first, hd, body = H.parse_headers(raw)
    cookie = hd.get(u'cookie')
    hdrs = [(u'Content-Type', u'text/plain')]
    if not cookie:
        sid = u's%02d' % (len(sessions) + 1)
        hdrs.append((u'Set-Cookie', u'SID=%s; Path=/; HttpOnly' % sid))
    else:
        sid = cookie.split(u'=')[1].split(u';')[0]
        sessions[sid] = sessions.get(sid, 0) + 1

实跑输出:

--------------------------------------------------------------------
Cookie:让无状态协议记住一个人
--------------------------------------------------------------------
  甲 第 1 次                 新客人,发给你 s01
    响应带 Set-Cookie        SID=s01; Path=/; HttpOnly
  甲 第 2 次(带 Cookie)    老客人 s01,第 1 次来
  甲 第 3 次(带 Cookie)    老客人 s01,第 2 次来
  乙 第 1 次(新连接无 Cookie) 新客人,发给你 s02

  服务端记下的会话数         2 个
  甲被认出的次数             3 次(含首次)
  同一个人的三次请求里,**只有后两次带 Cookie**;
  乙虽然是同一台机器、同一个浏览器进程,因为没有那个标识,就被当成新客人。
  结论:**识别身份靠客户端每次主动出示标识**,服务端只负责发和验。
  `HttpOnly` 让 JavaScript 读不到这个 Cookie,是防 XSS 偷 Cookie 的第一道墙。

甲 第 1 次 新客人,发给你 s01 → 响应带 Set-Cookie: SID=s01; Path=/; HttpOnly。 甲 第 2 次(带 Cookie)老客人 s01,第 1 次来 —— 认出来了。 注意计数从 1 开始,因为首次那次服务端只发了 Cookie、没算访问。 乙 第 1 次(新连接无 Cookie)新客人,发给你 s02 —— 同一台机器、同一个程序,没带那个字符串, 就是另一个人。 服务端记下的会话数 2 个、甲被认出的次数 3 次(含首次)。 这几个数把「Cookie 就是身份证」这件事说得最清楚。

30 秒实验

  1. 把 HttpOnly 去掉,想想 XSS 之后攻击者能拿到什么。
  2. 给 Cookie 加上 Max-Age=10,等 11 秒后再发请求,看客户端还在不在带它(这里要手动模拟, 因为我们是裸 socket)。
  3. 把 SameSite=Strict 加进 Set-Cookie,想想它会挡住哪些场景(从别的站点点链接过来, 第一次请求不带 Cookie)。

常见误区

  1. 「Cookie 是客户端的存储」——它是服务端设置、客户端保管并自动回传的, 不是给 JS 用的本地存储(那是 localStorage)。
  2. 「Cookie 能存很多」——单个域通常限 4 KB,总数也有限。大东西放不下。
  3. 「Cookie 加密了」——它默认是明文。所以必须 Secure + HTTPS。
自测三问(点开对答案) 1. 一次 Cookie 会话靠哪两个首部完成? —— 服务端发 `Set-Cookie`,客户端之后每次请求带 `Cookie`。 2. `HttpOnly` 防的是什么? —— 防 JavaScript 读取,也就是防 XSS 把会话标识偷走。 3. 为什么说 Cookie 有固定开销? —— 同域同路径的每个请求都会自动带上它,页面请求越多,重复传输越多。

第二章到此结束。第三章往报文内部走:编码、分块、多部分、范围请求。

相关笔记