📚 第八章 应用协议
📄 一句话概括

Cookie 是给无状态协议补「记忆」的机制:服务端下发,客户端自动回送。 JavaScript 跑在浏览器里,CGI 跑在服务器上,两者位置完全不同。

📝 本节和前面几节的关系

这一节用真的 cookiejar 跑一遍,让服务端把收到的 Cookie 头打出来。 再摆清 JavaScript、CGI、HTML 三者的分工。

一、Cookie 是怎么被自动带上的

整个过程只有两步: 服务端在响应头里塞一句 Set-Cookie;客户端存下来,之后每次请求自动附加。 注意「自动」这两个字:这个附加过程不经过 JavaScript, 是浏览器(或 urllib 的处理器)在发请求前做的。 所以你在开发者工具里能看到它,但你在页面代码里找不到是谁写的。 这也是「禁用 Cookie」会导致登录失效的原因: 服务端认的是那个 Cookie,不是你的 IP、不是你的浏览器指纹。 一旦客户端不回送,服务端每次看你都是新访客。

二、五个属性,两个和安全有关

Domain 管「发给哪些主机」、Path 管「发给哪些路径」、Expires 管「什么时候过期」。 不写 Expires 就是「关浏览器就没」,这类叫会话 Cookie。 后两个是安全属性,也是最常被漏掉的两个:Secure 要求只走 HTTPS, HttpOnly 要求 JavaScript 读不到。 没有 Secure 的后果很直接:同一个 Cookie 会在 HTTP 明文请求里裸奔, 路径上任何一个节点都能读到它。 所以「登录态 Cookie 必须带 Secure」是硬要求,这一条在几乎所有安全规范里都排在最前面。 没有 HttpOnly 的后果更隐蔽:一段注入的脚本就能把它读走,然后发到攻击者的服务器上。 加上 HttpOnly 之后,脚本连读都读不到,即使有 XSS,也偷不走登录态。 这是「纵深防御」的典型做法:假设第一道防线会被突破。

三、三样东西各跑在哪儿

HTML 跑在浏览器里,描述结构;JavaScript 跑在浏览器里,管行为; CGI 跑在服务器上,收请求、跑程序、拼 HTML 回去。 这三行就是 1993 年到 2005 年 Web 的全部架构: 页面是服务器拼好的,浏览器只负责显示和一点点校验。 所以那时候「表单验证」要在两边各写一遍—— 前端为了体验(不用等一次往返),后端为了安全(前端可以被绕过)。 今天的「前后端分离」把第二行放大了几十倍:HTML 变成了一张空壳,内容全靠 JavaScript 去取。 但底下的 HTTP 还是同一个 HTTP,Cookie 也还是同一套 Cookie。 所以「框架换了三代,协议没动」这个观察,是理解 Web 技术演进的最好角度。

四、动手 Demo:Cookie 是怎么被「自动带上」的

用真的 cookiejar 连发三次请求,服务端把收到的 Cookie 头记下来。 看第一次为什么是空的。

import http.cookiejar, urllib.request

seen = []
port, srv = start_server(record=seen)   # 服务端把 Cookie 头记进 seen

jar = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(
    urllib.request.HTTPCookieProcessor(jar))

for i in range(3):
    with opener.open('http://127.0.0.1:%d/p%d' % (port, i), timeout=5) as r:
        r.read()

for i, c in enumerate(seen, 1):
    print('第 %d 次:%s' % (i, c or '(没有 Cookie 头)'))

实跑输出:

--------------------------------------------------------------------
Cookie 是怎么被「自动带上」的
--------------------------------------------------------------------
**HTTP 无状态,于是有了 Cookie**:
**服务端在响应头里塞一句 `Set-Cookie`,浏览器存下来,之后每次请求自动回送。**
**下面用真的 cookiejar 跑一遍,并让服务端把收到的头打出来。**

**服务端收到的 Cookie 头,按请求顺序**:

    第 1 次请求:(没有 Cookie 头)
    第 2 次请求:sid=abc123
    第 3 次请求:sid=abc123

**第一次是空的,第二三次都带上了。**
**因为第一次响应的 `Set-Cookie` 被 jar 存了下来,之后自动附加。**
**这个过程不经过 JavaScript**,**是浏览器(或 urllib 的处理器)在发请求前自动附加的。**
**所以在开发者工具里能看到它,但你在页面代码里找不到是谁写的。**

**Cookie 有五个属性,各管一件事**:

  属性           管什么                             不写会怎样
  ---------------------------------------------------------------------------------
  `Domain`       **这个 Cookie 能发给哪些主机**     **不写就是「只发给当前主机」**
  `Path`         **能发给哪些路径**                 **写 `/` 就是全站**
  `Expires`      **什么时候过期**                   **不写就是「关浏览器就没」**
  `Secure`       **只走 HTTPS**                     **不加就会被明文链路泄露**
  `HttpOnly`     **JavaScript 读不到**              **防 XSS 偷 Cookie**

**后两个是安全属性,也是最常被漏掉的两个**:
**没有 `Secure`,同一个 Cookie 会在 HTTP 明文请求里裸奔。**
**没有 `HttpOnly`,一段注入的脚本就能把它读走。**

**再说 JavaScript 和 CGI 在这套体系里的位置**:

  东西               跑在哪             能做什么
  --------------------------------------------------------------------------
  **HTML**           **在浏览器里**     **描述结构,不算逻辑**
  **JavaScript**     **在浏览器里**     **能改页面、能发请求、能读 Cookie**
  **CGI**            **在服务器上**     **收请求、跑程序、拼 HTML 回去**

**这三行是 1993 年到 2005 年 Web 的全部架构**:
**页面是服务器拼好的,浏览器只负责显示和一点点校验。**
**今天的「前后端分离」把第二行放大了几十倍**——**HTML 变成了一张空壳,内容全靠 JavaScript 去取。**
**但底下的 HTTP 还是同一个 HTTP,Cookie 也还是同一套 Cookie。**

第一次是空的,第二三次都带上了 sid=abc123。 因为第一次响应的 Set-Cookie 被 jar 存了下来,之后自动附加。 这个「第二次才带上」的顺序很重要:它说明 Cookie 是「服务端先给、客户端后还」的单向流程。 所以「客户端能不能自己造一个 Cookie」这个问题的答案是「能」——Cookie 没有任何防伪机制, 服务端必须自己校验它的内容(签名或加密)。 这也是「Cookie 里不要放敏感明文」的原因:它只是一个「客户端可以随便改的字符串」, 不是「服务端存在你那里的凭证」。 所有「防篡改」的责任都在服务端。

30 秒实验

  1. 想一下:把 Cookie 删掉再请求一次,服务端还认得你吗?
  2. 想一下:HttpOnly 能防住什么?防不住什么?
  3. 想一下:为什么表单验证要在前后端各写一遍?

常见误区

  1. 以为「Cookie 只能由服务端设置」。 客户端也能造,所以服务端必须校验。
  2. 以为「加了 HttpOnly 就安全了」。 它只防读, 防不住 CSRF 这类「借用你的 Cookie 发请求」。
  3. 以为「Cookie 越大越好」。 每个请求都要带,太大会拖慢所有请求。
自测三问(点开对答案) 1. Cookie 的两步流程是什么? —— **服务端下发 `Set-Cookie`,客户端之后每次请求自动回送。** 2. `Secure` 和 `HttpOnly` 各防什么? —— **`Secure` 防明文链路泄露**;**`HttpOnly` 防脚本读取。** 3. CGI 和 JavaScript 各跑在哪? —— **CGI 在服务器上,JavaScript 在浏览器里。**

下一节换到网络管理:SNMP。

相关笔记