4.5 给 HTTP 定的性格是无状态——每个请求都是独立一张纸,服务器转过脸就不认识你。Cookie(教程里写作 HTTPCookie,日常就叫 Cookie) 就是给这个性格打的补丁,也是网站留在你电脑上的唯一一份文件:服务器在响应头 Set-Cookie 里塞给你一张小纸条,浏览器替你存下来,之后每次请求都自动用 Cookie 头带回去。纸条上最要紧的一点是——它通常只揣着一个编号(Session ID),真正的账本记在服务器那一侧(会话)。把"纸条"和"账本"分开这一点想通了,清 Cookie 为什么会被登出、偷走 Cookie 为什么能冒充你、广告凭什么跟着你跨站跑,全都顺理成章。
4.5 里 Cookie 露过两次脸,都只有一句话:请求头表里的 Cookie("我的身份凭证")、响应头表里的 Set-Cookie("给你一张身份证")。
4.6 从服务器那一侧看"装好包装",Set-Cookie 只是响应头清单里的一员。
本节把这张纸条单独拎出来当主角:它怎么来、长什么样、存在哪、谁管它、能干什么、又危险在哪。
一、为什么需要 Cookie
HTTP 无状态是故意设计的(收 4.5):换来的是简单、好扩展、任意一台机器都能接你下一个请求(收 1.7 讲的弹性伸缩)。
但代价也很具体——登录、购物车、多步表单、个性化推荐,没一样做得了。
举个最直白的例子,登录:浏览器每次都递上一张崭新的纸,服务器手上没有"这个人刚才通过了密码验证"的任何记录。你只要刷新一下,它就得再问你一遍密码。
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: SESSIONID=A7F3; theme=dark; lang=zh-CN
它的隔离边界只看域名(和路径),不看端口。所以本地开发时 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=/
Domain:这条 Cookie 归哪个域名。浏览器只把 Cookie 送回它归属的那个域名——所以a.com读不到b.com的 Cookie。域名是 Cookie 的隔离边界(收 3.21 的域名、4.4 的 URL 分段)Path:域名下的哪一段路径才带上。Path=/cart表示只在访问/cart...时才带;Path=/表示整站都带- 不写
Domain时,默认只归设置它的那一台主机(子域名不共享);写成Domain=.example.com(前面那个点)才表示连子域名一起共享
3. 寿命——会话 Cookie 与持久 Cookie
| 会话 Cookie(Session Cookie) | 持久 Cookie(Persistent Cookie) | |
|---|---|---|
| 写法 | 不写 Expires / Max-Age |
Max-Age=3600 或 Expires=… |
| 活多久 | 关掉浏览器就没了 | 到点才删 |
| 存在哪 | 只在内存里 | 落盘(本机上的一个文件) |
| 常见用途 | 登录凭证、临时状态 | "记住我"、语言/主题偏好 |
Max-Age是相对秒数:Max-Age=2592000就是 30 天Expires是绝对时间点:Expires=Wed, 21 Oct 2026 07:28:00 GMT- 两个都写时,
Max-Age说了算(Expires是给老浏览器兜底的) - 删一条 Cookie 的土办法:把
Max-Age设成0,让它立刻过期——网站上那个"退出登录"按钮,背后常常就这一下
4. 三条安全规矩
Set-Cookie: SESSIONID=A7F3; Secure; HttpOnly; SameSite=Lax
Secure:只有走 HTTPS 才带这张纸条。不加的话,用 http 明文访问时它会在路上裸奔(收 4.4:协议段决定加不加密)HttpOnly:页面里的脚本(JS)读不到它。为什么重要——万一页面被塞进了恶意脚本,它想偷document.cookie也偷不到这一条。放登录凭证的 Cookie 一定要加SameSite:别人家网站发起的请求,还要不要带上我的纸条。Lax(默认值:点了链接导航过去会带,跨站发的请求不带)/Strict(一律不带)/None(都带,但此时必须配Secure)——这一条是专门为挡 CSRF 造的(第五节)
服务器收到请求时,只看得到 Cookie: SESSIONID=A7F3 这一段。
Domain、Path、Max-Age、Secure、HttpOnly、SameSite —— 一个字节都不会发过去。它们全是浏览器自己执行的保管和投放规矩。
所以:想让浏览器改行为,改的是响应头里那份写法,而不是请求。
四、服务器那边记什么——"会话"
这是本节最该理清的一层。
Cookie 里放编号,账本放在服务器那一侧。 这个"账本"就叫会话(Session):
| Cookie | 会话(Session) | |
|---|---|---|
| 存在哪 | 你的电脑(浏览器管理) | 服务器(内存,或 Redis 之类的存储) |
| 存什么 | 一个编号(SESSIONID=A7F3) |
真正的数据:你是谁、购物车里有什么、几点登录的 |
| 谁能看见 | 你自己就能看(F12 里一目了然) | 用户看不到 |
| 谁能改 | 你改了也没用——编号对不上号,服务器就当没这个人 | 只有服务器能改 |
于是几个日常现象一次说清:
- 清掉 Cookie 就被登出:不是服务器注销了你的账号,而是它再也认不出这张纸条对应哪个账本
- 换机器 / 换浏览器要重新登录:纸条在本机
- 服务器重启后有时也会被登出:账本存在内存里,一重启就没了(存进 Redis 之类的外部存储就不会)
- 同一台电脑能同时登两个账号:网站给每个新会话发不同的编号,两张纸条并存(开无痕窗口常能同时登第二个号)
- Cookie 被偷 = 身份被偷:偷的人不需要知道你的密码,他手上有那张纸条
会话账本有寿命(服务器设定,比如 30 分钟不操作就清掉),Cookie 也有寿命(Max-Age)。两边都活着,你才是"登录状态"。
3.23 讲 DHCP 时说过"每样东西都有寿命",DNS 的 TTL(收 3.21)、HTTP 的缓存(收 4.6)也都是这个思路——网络里到处都是"有效期"这个机制。
五、Cookie 是谁的财产
第一方 vs 第三方
- 第一方 Cookie:地址栏里那个站自己发的 —— 登录、偏好设置都靠它,必要且正常
- 第三方 Cookie:你访问 A 站,页面里嵌着 B 家的东西(广告位、统计脚本、字体),B 也能给你发 Cookie。于是 B 在你访问过的很多站上都认得你 → 跨站跟踪的燃料
这也解释了一个熟悉的现象:搜过一次的东西,接下来在别的网站反复出现。
三个风险与对应的挡法
| 风险 | 怎么来的 | 常见挡法 |
|---|---|---|
| 偷走 Cookie 冒充你 | Cookie 在请求里明文走,中途被截走就完了 | Secure(只走 HTTPS);别在公共 Wi-Fi 上乱登 |
| 页面脚本偷 Cookie | 页面被注入恶意 JS,读 document.cookie 回传 |
HttpOnly |
| CSRF(借你的手办事) | 你在别站点了链接,浏览器照样把纸条带上,服务器以为是你本人 | SameSite + 表单加令牌 |
Cookie 是"凭证",不是"密码"。 它只能证明"这个浏览器刚才登录过",不能证明"现在按按钮的是本人"。
所以凡是花钱、改密码、删账号这种动作,靠谱的网站都会再问你一次(重输密码、短信验证码)。
六、在你的浏览器里看见它、改它、删它
- 看:F12 → 应用(Application) → 左侧 Cookie → 按域名分组;每条的 Name / Value / Domain / Path / Expires / HttpOnly / Secure / SameSite 都列出来了
- 改:双击 Value 直接改,刷新页面 —— 请求头里的
Cookie跟着变。这一下同时证明了:Cookie 存在你这边、服务器眼里的"你是谁"全看这张纸条 - 删:右键删除,或者用"清除浏览数据 → Cookie"。删完刷新,网站就把你当陌生人
- 对上:网络面板里找请求头
Cookie和响应头Set-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 秒实验
- 30 秒:随便开一个网站,F12 → 应用 → Cookie,数一数这个域名下有几条。
- 30 秒:网络面板找一条请求,看响应头
Set-Cookie与下一条同域请求的Cookie,一进一出接上。 - 30 秒:找一条带
HttpOnly的 Cookie,回到 Console 敲document.cookie—— 它不在里面(HttpOnly正在干活)。
九、常见误区
- "Cookie 是服务器上的一段数据。" —— Cookie 在你电脑上(浏览器管理);服务器那一侧存的是会话。
- "HTTP 记得我,所以不用重新登录。" —— HTTP 无状态,记得你的是 Cookie(收 4.5)。
- "Cookie 里存着用户名密码。" —— 正常只存一个编号。把密码写进 Cookie 是重大事故。
- "Cookie 是程序,会自己执行。" —— 它只是一串字符串。危险不在它会"跑",而在它被当成身份凭证用。
- "所有 Cookie 关掉浏览器都会消失。" —— 那是会话 Cookie。写了
Max-Age/Expires的会留在硬盘上。 - "清 Cookie 只是登出而已。" —— 顺手也清了语言、主题、字号这些偏好,下一次打开全变回默认。
- "A 网站的 Cookie 能被 B 网站读走。" —— 直接读不到(域名隔离)。但第三方 Cookie 能让 B 在很多站上都认出你,这是另一种性质的问题。
- "加了
HttpOnly就万无一失。" —— 它只挡"脚本读 Cookie",挡不了 CSRF,也挡不了抓包(那是Secure的活)。 - "换台电脑用同一个浏览器,登录状态能同步。" —— 除非你登录了浏览器账号做同步,否则纸条在原机器上。
- "Cookie 当然是越多越方便。" —— 每个请求都要背着全部 Cookie 跑,越多越慢;而且浏览器对单域名 Cookie 条数和单条大小都有上限,超了会被悄悄丢掉。
十、动手自测
- HTTP 为什么需要 Cookie 这个补丁?"无状态"换来了什么好处?
- 一条 Cookie 从服务器到你电脑、再回到服务器,各自走的是哪个头?
- Cookie 里通常存的是什么?真正的"账本"存在哪?
Domain和Path各管什么?不写Domain时,子域名为什么读不到?- 会话 Cookie 和持久 Cookie 怎么区分?
Max-Age和Expires有什么不同? - 想让浏览器立刻丢掉一条 Cookie,服务器可以怎么做?
Secure、HttpOnly、SameSite分别挡什么?- 收到请求时,服务器能看到
Max-Age、Path这些属性吗? - 为什么清掉 Cookie 会被登出?换一台电脑为什么也要重新登录?
- 第一方 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: 名字=值,属性是浏览器执行的保管规矩,一个字节都不发给服务器。
⑨ 因为登录状态是"你手上的纸条"和"服务器上的账本"两头对号;纸条没了或换了机器,服务器认不出这张纸条对应哪个账本,只好把你当新访客。
⑩ 第一方是地址栏里那个站自己发的(登录、偏好,必要且正常);第三方是页面里嵌的别人家内容(广告、统计、字体)发的,能跨很多个站认出同一个你,是跨站跟踪的燃料。
十一、下一步学什么
- ☑ 对Web网站的访问进行代理(代理服务器) —— 本节把 4.5 留下的"那张纸条"讲透了,也把"会话"这笔账理清了。这就换个位置看"中间人":代理服务器——它站在应用层,看得懂 HTTP,所以能替你去取、能缓存、能管控;顺带讲清最容易被名字骗过去的一对概念:正向代理替客户端出面,反向代理替服务器出面。
相关笔记
- 传输Web网站的文件(HTTP响应) —— 上一节:
Set-Cookie就在它讲的那串响应头里;本节是它的展开 - 请求Web网站的文件(HTTP) —— "无状态"这一性格的出处;Cookie 是给它的补丁
- Web网站的地址(URL) ——
Domain/Path的边界概念,来自 URL 的域名段与路径段;Secure与协议段对应 - Web网站是如何构建的(Web网站) —— 网站的四个部件都在服务器上,Cookie 是唯一的例外;"动态内容"背后那套程序,正是会话账本的所在
- 运行和管理服务器(服务器、云服务、内部部署) —— 无状态为什么是优点(弹性伸缩);会话账本存在内存还是外部存储,结果大不一样
- 网络的电话簿(域名解析、主机名、DNS) —— 域名是 Cookie 的隔离边界;"有效期"这个思路从 DNS 的 TTL 一脉相承到 Cookie 的
Max-Age - 根据端口号分配数据到应用程序(端口号、已知端口号) —— 端口负责区分应用,但 Cookie 不看端口,本地多端口开发时要注意