🌐 第四章 Web网站的运行原理
📄 一句话概括

4.5 给 HTTP 定的性格是无状态——每个请求都是独立一张纸,服务器转过脸就不认识你。Cookie(教程里写作 HTTPCookie,日常就叫 Cookie) 就是给这个性格打的补丁,也是网站留在你电脑上的唯一一份文件:服务器在响应头 Set-Cookie 里塞给你一张小纸条,浏览器替你存下来,之后每次请求都自动用 Cookie 头带回去。纸条上最要紧的一点是——它通常只揣着一个编号(Session ID),真正的账本记在服务器那一侧(会话)。把"纸条"和"账本"分开这一点想通了,清 Cookie 为什么会被登出、偷走 Cookie 为什么能冒充你、广告凭什么跟着你跨站跑,全都顺理成章。

📝 本节和 4.5 / 4.6 的分工

4.5 里 Cookie 露过两次脸,都只有一句话:请求头表里的 Cookie("我的身份凭证")、响应头表里的 Set-Cookie("给你一张身份证")。

4.6 从服务器那一侧看"装好包装",Set-Cookie 只是响应头清单里的一员。

本节把这张纸条单独拎出来当主角:它怎么来、长什么样、存在哪、谁管它、能干什么、又危险在哪。

一、为什么需要 Cookie

HTTP 无状态是故意设计的(收 4.5):换来的是简单、好扩展、任意一台机器都能接你下一个请求(收 1.7 讲的弹性伸缩)。

但代价也很具体——登录、购物车、多步表单、个性化推荐,没一样做得了。

举个最直白的例子,登录:浏览器每次都递上一张崭新的纸,服务器手上没有"这个人刚才通过了密码验证"的任何记录。你只要刷新一下,它就得再问你一遍密码。

Cookie 就是在"无状态"的地基上,硬造出一层"有记忆"的表象。

📝 换个角度想:Cookie 是"网站留在你电脑上的文件"

收 4.1 讲过——网站的四个部件(域名、主机、Web 服务器软件、内容文件)全在服务器那一侧,你的电脑上什么都不用装。

Cookie 是唯一的例外:它是网站放到你本机、下次还要来取回的一小份数据。所以本节的标题叫"记录 Web 网站的文件"。

这一个定位解释了一串日常怪事:换台电脑就要重新登录(纸条还在原来那台机器上)、无痕窗口关掉就登出(无痕模式不落盘)、清了 Cookie 就被当陌生人(纸条没了)、同一个网站能同时登两个账号(两张纸条同时存在)。

二、来回一趟:纸条怎么发、怎么带

(配图一)

第一次:你不认识我

浏览器递上去的请求里,根本没有 Cookie 这个头:

GET /index.html HTTP/1.1
Host: shop.example.com
(没有 Cookie 头)

服务器一查:没纸条 → 现场生成一个新编号 → 在响应头里发一张:

HTTP/1.1 200 OK
Set-Cookie: SESSIONID=A7F3
Content-Type: text/html; charset=utf-8

第二步:浏览器存下来

浏览器按域名把 SESSIONID=A7F3 记进本机的 Cookie 存储(F12 → 应用 → Cookie 里能看见)。

这一步和上一步都是浏览器在替你办。网站没有权限直接往你硬盘上写东西,它只能借响应头提个要求,落盘与否是浏览器自己的事。

第二次:我认得你了

之后的每个请求,浏览器都会自动把归属于该域名的 Cookie 塞进 Cookie 头:

GET /cart HTTP/1.1
Host: shop.example.com
Cookie: SESSIONID=A7F3
⚠️ 记住这一条,Cookie 的脾气全能推出来

纸条是浏览器主动翻出来带上的,不是服务器来取的。

  • 浏览器照什么规则带(域名对不对、路径对不对、过没过期)→ 由纸条上那些属性决定(第三节)
  • 纸条存在你的电脑上 → 你能看、能改、能删
  • 送过去的东西不一定可信 → 服务器不敢拿 Cookie 里的内容当命令执行
  • 别的站能蹭着带 → 这就是"第三方 Cookie"的由来(第五节)

范围和顺序:Cookie 头里会出现该域名下所有还没过期的条目,用 ; 分隔:

Cookie: SESSIONID=A7F3; theme=dark; lang=zh-CN
💡 顺带一提:Cookie 不认端口

它的隔离边界只看域名(和路径),不看端口。所以本地开发时 http://localhost:8000 和 http://localhost:3000 会共享同一批 Cookie——两个项目用同一个 Cookie 名时会互相踩,这是本地调试常见的怪事之一(收 3.17:端口的职责是区分应用,Cookie 这一层没管它)。

⚠️ 它每个请求都跟着跑,不嫌浪费吗

是的,每个请求都带——包括取一张小图这种小事。装进去的东西越多,每个请求都越臃肿。

这也是"Cookie 只放编号、不放数据"最现实的原因之一(第四节)。

三、纸条上写了什么

(配图二)

1. 名值对——服务器真正要认的那部分

SESSIONID=A7F3

就是一对"名字=值"。值只能是字符串——放不了对象、放不了二进制。想塞结构化数据得自己序列化(比如塞一段 JSON),但不推荐:一是每个请求都背着它跑,二是它保存在用户机器上,不可信(第四节讲该放哪)。

2. Domain / Path——这张纸条归谁

Set-Cookie: SESSIONID=A7F3; Domain=shop.example.com; Path=/

3. 寿命——会话 Cookie 与持久 Cookie

会话 Cookie(Session Cookie) 持久 Cookie(Persistent Cookie)
写法 不写 Expires / Max-Age Max-Age=3600 或 Expires=…
活多久 关掉浏览器就没了 到点才删
存在哪 只在内存里 落盘(本机上的一个文件)
常见用途 登录凭证、临时状态 "记住我"、语言/主题偏好

4. 三条安全规矩

Set-Cookie: SESSIONID=A7F3; Secure; HttpOnly; SameSite=Lax
💡 属性是给浏览器看的,不是给服务器看的

服务器收到请求时,只看得到 Cookie: SESSIONID=A7F3 这一段。

Domain、Path、Max-Age、Secure、HttpOnly、SameSite —— 一个字节都不会发过去。它们全是浏览器自己执行的保管和投放规矩。

所以:想让浏览器改行为,改的是响应头里那份写法,而不是请求。

四、服务器那边记什么——"会话"

这是本节最该理清的一层。

Cookie 里放编号,账本放在服务器那一侧。 这个"账本"就叫会话(Session):

Cookie 会话(Session)
存在哪 你的电脑(浏览器管理) 服务器(内存,或 Redis 之类的存储)
存什么 一个编号(SESSIONID=A7F3) 真正的数据:你是谁、购物车里有什么、几点登录的
谁能看见 你自己就能看(F12 里一目了然) 用户看不到
谁能改 你改了也没用——编号对不上号,服务器就当没这个人 只有服务器能改

于是几个日常现象一次说清:

📝 这一条也顺手解释了"为什么登录状态能维持很久"

会话账本有寿命(服务器设定,比如 30 分钟不操作就清掉),Cookie 也有寿命(Max-Age)。两边都活着,你才是"登录状态"。

3.23 讲 DHCP 时说过"每样东西都有寿命",DNS 的 TTL(收 3.21)、HTTP 的缓存(收 4.6)也都是这个思路——网络里到处都是"有效期"这个机制。

五、Cookie 是谁的财产

第一方 vs 第三方

这也解释了一个熟悉的现象:搜过一次的东西,接下来在别的网站反复出现。

三个风险与对应的挡法

风险 怎么来的 常见挡法
偷走 Cookie 冒充你 Cookie 在请求里明文走,中途被截走就完了 Secure(只走 HTTPS);别在公共 Wi-Fi 上乱登
页面脚本偷 Cookie 页面被注入恶意 JS,读 document.cookie 回传 HttpOnly
CSRF(借你的手办事) 你在别站点了链接,浏览器照样把纸条带上,服务器以为是你本人 SameSite + 表单加令牌
⚠️ 一句话记牢

Cookie 是"凭证",不是"密码"。 它只能证明"这个浏览器刚才登录过",不能证明"现在按按钮的是本人"。

所以凡是花钱、改密码、删账号这种动作,靠谱的网站都会再问你一次(重输密码、短信验证码)。

六、在你的浏览器里看见它、改它、删它

🚨 别把 Cookie 的值发给别人

有些 Cookie 的值本身就是登录凭证——拿到它,等于拿到你的登录状态。

截图、贴日志、发给任何人(包括发给 AI 分析)时,都要把它遮掉。

七、动手 Demo:亲手演一遍"发纸条、带纸条"

准备:新建 cookie_demo.py —— 一个会自动发 Cookie 的最小服务器

from http.server import BaseHTTPRequestHandler, HTTPServer

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        got = self.headers.get("Cookie")
        if got:
            body = "认得你了。你带回来的纸条是:" + got
            extra = []
        else:
            body = "第一次见面 —— 已给你一张纸条,按 F5 刷新看看"
            extra = [("Set-Cookie", "SESSIONID=A7F3; Max-Age=3600; Path=/")]
        raw = body.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(raw)))
        for k, v in extra:
            self.send_header(k, v)
        self.end_headers()
        self.wfile.write(raw)

HTTPServer(("127.0.0.1", 8000), H).serve_forever()

第 1 步 · 跑起来

python cookie_demo.py

浏览器打开 http://127.0.0.1:8000 —— 显示"第一次见面"。

第 2 步 · 抓住纸条发出的那一刻

F12 → 网络 → 刷新 → 点第一条请求 → 响应头里找这一行:

Set-Cookie: SESSIONID=A7F3; Max-Age=3600; Path=/

这就是 4.6 里那串响应头的现场版:服务器不跟你商量,直接把纸条塞进响应。

第 3 步 · 刷新,看纸条自己回来了

按 F5 —— 页面变成"认得你了"。同一请求的请求头里出现了:

Cookie: SESSIONID=A7F3

注意看:请求里既没有 Max-Age 也没有 Path。 属性被浏览器吃掉了——第三节那条 tip 的实证。

第 4 步 · 看它到底存在哪

F12 → 应用 → Cookie → http://127.0.0.1:8000 —— 一条 SESSIONID=A7F3,Expires 是"一小时后"。

这就是"网站留在你电脑上的文件"。

第 5 步 · 亲手改它

把 Value 改成 HACKED → F5 → 页面显示"你带回来的纸条是:SESSIONID=HACKED"。

服务器没有校验、也没有拒绝——它只是照着纸条认人。这一下就明白了:Cookie 里的内容本身没有防伪能力(所以真正要紧的凭证,必须由服务器核对或带签名)。

第 6 步 · 用命令行看"原始件"

curl -c cookie.txt -v http://127.0.0.1:8000    # -c:把收到的 Cookie 存进文件
cat cookie.txt                                  # 看一眼这个文件长什么样
curl -b cookie.txt -v http://127.0.0.1:8000    # -b:带着它再去一次

第一次的响应里能看到 Set-Cookie,第二次的请求里能看到一整个 Cookie: 行。

第 7 步 · 把纸条撕掉

在应用面板里删掉那条 → F5 → 又变回"第一次见面",而且服务器当场又发了一张新的。

这就是"清了 Cookie 就被登出"的全部机制——没有玄学。

八、30 秒实验

  1. 30 秒:随便开一个网站,F12 → 应用 → Cookie,数一数这个域名下有几条。
  2. 30 秒:网络面板找一条请求,看响应头 Set-Cookie 与下一条同域请求的 Cookie,一进一出接上。
  3. 30 秒:找一条带 HttpOnly 的 Cookie,回到 Console 敲 document.cookie —— 它不在里面(HttpOnly 正在干活)。

九、常见误区

  1. "Cookie 是服务器上的一段数据。" —— Cookie 在你电脑上(浏览器管理);服务器那一侧存的是会话。
  2. "HTTP 记得我,所以不用重新登录。" —— HTTP 无状态,记得你的是 Cookie(收 4.5)。
  3. "Cookie 里存着用户名密码。" —— 正常只存一个编号。把密码写进 Cookie 是重大事故。
  4. "Cookie 是程序,会自己执行。" —— 它只是一串字符串。危险不在它会"跑",而在它被当成身份凭证用。
  5. "所有 Cookie 关掉浏览器都会消失。" —— 那是会话 Cookie。写了 Max-Age / Expires 的会留在硬盘上。
  6. "清 Cookie 只是登出而已。" —— 顺手也清了语言、主题、字号这些偏好,下一次打开全变回默认。
  7. "A 网站的 Cookie 能被 B 网站读走。" —— 直接读不到(域名隔离)。但第三方 Cookie 能让 B 在很多站上都认出你,这是另一种性质的问题。
  8. "加了 HttpOnly 就万无一失。" —— 它只挡"脚本读 Cookie",挡不了 CSRF,也挡不了抓包(那是 Secure 的活)。
  9. "换台电脑用同一个浏览器,登录状态能同步。" —— 除非你登录了浏览器账号做同步,否则纸条在原机器上。
  10. "Cookie 当然是越多越方便。" —— 每个请求都要背着全部 Cookie 跑,越多越慢;而且浏览器对单域名 Cookie 条数和单条大小都有上限,超了会被悄悄丢掉。

十、动手自测

❓ 自测一下
  1. HTTP 为什么需要 Cookie 这个补丁?"无状态"换来了什么好处?
  2. 一条 Cookie 从服务器到你电脑、再回到服务器,各自走的是哪个头?
  3. Cookie 里通常存的是什么?真正的"账本"存在哪?
  4. Domain 和 Path 各管什么?不写 Domain 时,子域名为什么读不到?
  5. 会话 Cookie 和持久 Cookie 怎么区分?Max-Age 和 Expires 有什么不同?
  6. 想让浏览器立刻丢掉一条 Cookie,服务器可以怎么做?
  7. Secure、HttpOnly、SameSite 分别挡什么?
  8. 收到请求时,服务器能看到 Max-Age、Path 这些属性吗?
  9. 为什么清掉 Cookie 会被登出?换一台电脑为什么也要重新登录?
  10. 第一方 Cookie 和第三方 Cookie 的区别是什么?

答案: ① 因为 HTTP 是无状态的——每个请求独立,服务器不记得上一个(换来的是简单、好扩展、任意机器都能接,收 4.5 / 1.7)。而登录、购物车、多步表单都要求"记得住",Cookie 就是补这一层记忆的。 ② 发下来走响应头 Set-Cookie;带回去走请求头 Cookie。 ③ Cookie 里通常只存一个编号(如 SESSIONID=A7F3)。真正的账本(你是谁、购物车里有什么)存在服务器的会话(Session)里。 ④ Domain 管归属哪个域名(决定"能不能发给你"),Path 管域名下哪一段路径才带(决定"哪次请求才带")。不写 Domain 时,这条 Cookie 只归设置它的那台主机,所以子域名(a.example.com 对 example.com)拿不到。 ⑤ 不写 Expires / Max-Age 的就是会话 Cookie(关浏览器就没,只在内存里);写了的是持久 Cookie(落盘保存)。Max-Age 是相对秒数,Expires 是绝对时间点,两个都写时 Max-Age 优先。 ⑥ 把 Max-Age 设为 0(或把一个 Expires 设成过去的时间),浏览器收到后立刻过期删除——"退出登录"常用这一手。 ⑦ Secure 挡"明文路上被截"(只走 HTTPS 才带);HttpOnly 挡"页面脚本偷读 Cookie";SameSite 挡"别的站借你的手发请求"(CSRF)。 ⑧ 看不到。 请求里只有 Cookie: 名字=值,属性是浏览器执行的保管规矩,一个字节都不发给服务器。 ⑨ 因为登录状态是"你手上的纸条"和"服务器上的账本"两头对号;纸条没了或换了机器,服务器认不出这张纸条对应哪个账本,只好把你当新访客。 ⑩ 第一方是地址栏里那个站自己发的(登录、偏好,必要且正常);第三方是页面里嵌的别人家内容(广告、统计、字体)发的,能跨很多个站认出同一个你,是跨站跟踪的燃料。

十一、下一步学什么

相关笔记