登录态、跨域与缓存的暗战:Cookie / 同源策略 / 缓存实战
实验环境:Ubuntu 24.04、nginx 1.24.0、curl 8.5.0、dnsutils(dig)
服务器公网 IP:120.46.193.105;跨域演示用同机两个端口8080(API)与8081(页面)
本文所有响应头、状态码、抓包结果均为真实执行,关键行已加注释。
0. 引言:这些"玄学"你遇到过吗
- 登录态丢失:明明
Set-Cookie返回了,刷新页面却还是未登录——多半是SameSite/Secure拦了,或HttpOnly被你当成了 bug。 - 跨域报错:前端
fetch一个接口,浏览器控制台红一片Blocked by CORS policy,但curl明明能拿到数据。 - 更新不生效:改了 CSS/JS,用户说"还是旧的"——缓存没失效,你却不知道是哪条
Cache-Control在作怪。 - POST 重定向丢参(上一篇的坑):用
301/302重定向一个POST,服务端收不到参数——因为方法被偷偷改成了GET。
这四个问题,分别对应Cookie、同源策略(CORS)、缓存、重定向语义。本篇全部用真实报文拆给你看。
1. 实验环境补充
在上一篇的基础上,本篇额外启用两个端口:
# API 站点(8080):返回跨域头 server { listen 8080; location /api { add_header Access-Control-Allow-Origin "http://120.46.193.105:8081"; add_header Access-Control-Allow-Credentials "true"; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, X-Custom"; if ($request_method = OPTIONS) { # 预检请求直接 204 add_header Access-Control-Allow-Max-Age "600"; return 204; } return 200 "api response from 8080\n"; } } # 页面站点(8081):模拟"另一个源" server { listen 8081; }说明:
http://120.46.193.105:8080与http://120.46.193.105:8081端口不同 = 不同源(同源策略看 协议+域名+端口 三者全同),正好用来演示跨域。
2. 一、Cookie:服务端怎么"记住"你
HTTP 本身无状态。Cookie机制让客户端在后续请求里自动带回服务端下发的标识。
2.1 服务端下发:Set-Cookie及其属性
curl-svhttp://127.0.0.1/setcookie真实输出:
< HTTP/1.1 200 OK < Content-Length: 11 < Connection: keep-alive < Set-Cookie: sid=abc123; Max-Age=3600; HttpOnly; SameSite=Lax; Path=/ < Set-Cookie: theme=dark; Max-Age=60 < cookie set再看一个"更严格"的 Cookie(Secure+SameSite=Strict):
curl-svhttp://127.0.0.1/setcookie_secure真实输出:
< Set-Cookie: token=xyz; Secure; HttpOnly; SameSite=Strict; Max-Age=120属性逐条解读:
| 属性 | 作用 | 生产建议 |
|---|---|---|
Max-Age=3600 | Cookie 存活秒数(相对值);另有Expires=绝对时间 | 会话型用短时效,记住我功能才用长时效 |
HttpOnly | 禁止 JS 通过document.cookie读取 | 防 XSS 偷 cookie,必须给认证 cookie 加 |
Secure | 仅通过 HTTPS 传输 | 明文 HTTP 下带Secure的 cookie 不会被发送 |
SameSite=Lax/Strict/None | 控制跨站请求是否带 cookie | 现代浏览器默认Lax;跨站带凭证必须None且配合Secure |
Path=/ | Cookie 生效路径范围 | 一般给根路径 |
2.2 客户端回传:Cookie请求头 + curl 的-c/-b
服务端写完,curl可以用-c把 cookie 存进"jar"文件,再用-b在下次请求里回传:
curl-s-c/tmp/cj.txt-o/dev/null http://127.0.0.1/setcookieecho"----- 保存的 cookie jar -----"cat/tmp/cj.txt真实输出:
# Netscape HTTP Cookie File # https://curl.se/docs/http-cookies.html # This file was generated by libcurl! Edit at your own risk. 127.0.0.1 FALSE / FALSE 1784955328 theme dark #HttpOnly_127.0.0.1 FALSE / FALSE 1784958868 sid abc123注意
sid那一行开头的#HttpOnly_前缀:curl 在 jar 里也标记了这个 cookie 是 HttpOnly,意思是即使它在文件里,JS 也读不到——这正是HttpOnly的安全意义。
回传请求(用--trace-ascii看发出的Cookie头):
curl-s-b/tmp/cj.txt --trace-ascii - http://127.0.0.1/setcookie真实输出关键字节:
=> Send header, 113 bytes (0x71) 0000: GET /setcookie HTTP/1.1 0019: Host: 127.0.0.1 0042: Accept: */* 004f: Cookie: theme=dark; sid=abc123 # ← 浏览器/curl 自动回传 006f:浏览器行为小结:
1. 响应里出现 Set-Cookie → 浏览器按属性存起来 2. 之后同域请求 → 自动在 Cookie 头里带上(无需 JS 参与) 3. HttpOnly 的 cookie → JS 读不到,但请求照常带(安全) 4. Secure 的 cookie → 非 HTTPS 页面不会发送 5. SameSite=Strict → 跨站(含跨域表单提交)请求不带,抗 CSRF3. 二、同源策略与 CORS:浏览器为什么拦你
同源策略(Same-Origin Policy):浏览器禁止页面对"不同源"的资源做受保护读写。协议://域名:端口三者完全一致才同源。
CORS(Cross-Origin Resource Sharing)是服务端用一组响应头,显式授权哪些外源可以访问。
3.1 简单请求:带Origin头,服务端回Access-Control-Allow-Origin
curl-s-D--o/dev/null-H"Origin: http://120.46.193.105:8081"\http://127.0.0.1:8080/api真实输出:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://120.46.193.105:8081 # ← 授权这个源 Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Custom浏览器看到ACAO等于自己的源,于是放行。注意:我们没有带Origin时这个头也出现了——因为本实验的 nginx 配置是"无条件返回",这是不严谨的做法(正确做法是校验Origin是否在白名单内,再决定是否返回该头)。生产环境务必做白名单校验,否则等于对任何源开放。
3.2 预检请求(Preflight):OPTIONS先探路
当请求"不简单"(比如带自定义头X-Custom、或用PUT/DELETE、或Content-Type: application/json),浏览器会先发一个OPTIONS预检,问服务端"我能不能这么发",得到许可后才发真正的请求。
curl-s-i-XOPTIONS\-H"Origin: http://120.46.193.105:8081"\-H"Access-Control-Request-Method: POST"\-H"Access-Control-Request-Headers: X-Custom"\http://127.0.0.1:8080/api真实输出:
HTTP/1.1 204 No Content Access-Control-Allow-Origin: http://120.46.193.105:8081 Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Custom Access-Control-Max-Age: 600 # ← 预检结果缓存 600 秒,期间不再预检预检通过后再发真正的POST:
curl-s-i-XPOST-H"Origin: http://120.46.193.105:8081"\-H"X-Custom: 1"-d'k=v'http://127.0.0.1:8080/api真实输出关键行:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://120.46.193.105:8081 Access-Control-Allow-Credentials: trueCORS 流程 ASCII 图:
浏览器(8081 页面) 服务端(8080 API) │ │ │ ① OPTIONS 预检(问能不能发)│ │ ───────────────────────────► │ │ ② 204 + ACAO/ACAM/ACAH │ │ ◄─────────────────────────── │ │ ③ 真正 POST(带 Origin) │ │ ───────────────────────────► │ │ ④ 200 + ACAO(放行) │ │ ◄─────────────────────────── │生产踩坑:预检失败最常见原因是 nginx 没放行
OPTIONS方法,或没返回Access-Control-Allow-Headers里声明的自定义头。Access-Control-Allow-Origin不支持通配*与Credentials: true同时出现——带凭证时必须写具体源。
4. 三、条件请求与 304:让"没变"的资源别再传一遍
浏览器/代理缓存了一份资源,下次怎么判断"服务端内容变没变"?靠条件请求:带上之前拿到的校验器,服务端比对,没变就回304 Not Modified,不传包体。
4.1 两类校验器:Last-Modified 与 ETag
curl-s-D--o/dev/null http://127.0.0.1/cond|grep-i"etag\|last-modified\|HTTP"真实输出(首次请求,返回校验器):
HTTP/1.1 200 OK Last-Modified: Sat, 25 Jul 2026 04:51:01 GMT # ← 文件最后修改时间 ETag: "6a6440b5-63" # ← 实体标签(内容哈希/版本号)4.2 带上校验器再请求 → 304
# 用 ETag 比对curl-s-o/dev/null-w'status=%{http_code}\n'\-H'If-None-Match: "6a6440b5-63"'http://127.0.0.1/cond# 用 Last-Modified 比对curl-s-o/dev/null-w'status=%{http_code}\n'\-H'If-Modified-Since: Sat, 25 Jul 2026 04:51:01 GMT'http://127.0.0.1/cond真实输出:
status=304 status=304两者都返回304,响应没有包体,浏览器直接复用本地缓存,省下带宽与延迟。
4.3 校验器不匹配 → 200(返回新内容)
curl-s-o/dev/null-w'status=%{http_code}\n'\-H'If-None-Match: "deadbeef"'http://127.0.0.1/cond真实输出:
status=200ETag 对不上,服务端知道内容变了,于是正常返回200与新包体。
首次请求: GET /cond ─► 200 + Last-Modified + ETag + 包体 再次请求: GET /cond + If-None-Match:"xxx" ─► 304(无包体,用缓存) GET /cond + If-None-Match:"yyy"(不匹配) ─► 200 + 新包体
ETag比Last-Modified更精确(秒级时间可能重合),优先级更高;二者可并存,服务端任一不匹配即回 200。
5. 四、Cache-Control:缓存的"宪法"
Cache-Control是控制缓存行为的核心响应头。我们在 nginx 上分别配置了不同指令,逐一实测。
5.1 各指令实测
forepinmaxage nocache nostore private;doecho"----- /cache/$ep-----"curl-s-D--o/dev/null http://127.0.0.1/cache/$ep|grep-i"cache-control\|HTTP"done真实输出:
----- /cache/maxage ----- HTTP/1.1 200 OK Cache-Control: max-age=30, public ----- /cache/nocache ----- HTTP/1.1 200 OK Cache-Control: no-cache ----- /cache/nostore ----- HTTP/1.1 200 OK Cache-Control: no-store ----- /cache/private ----- HTTP/1.1 200 OK Cache-Control: private, max-age=605.2 指令语义
| 指令 | 含义 | 是否缓存 |
|---|---|---|
max-age=30 | 缓存 30 秒内视为新鲜,直接用不询问 | 是(时效内) |
public | 任何缓存(浏览器、CDN、代理)都可存 | — |
private | 仅用户浏览器可存,共享缓存(CDN)不可 | 仅私有缓存 |
no-cache | 可以缓存,但每次用之前必须回源校验(条件请求) | 是(强校验) |
no-store | 禁止任何缓存,每次都重新下载 | 否 |
5.3 新鲜度(freshness)怎么算
缓存是否"新鲜"取决于:
响应时间 now │ │ ├──► fresh 区间 = max-age 秒 ──► 直接用缓存,不发请求 │ └──► 超过 max-age ──► 过期 ├─ no-cache / 带校验器 → 发条件请求,304 则继续用缓存 └─ no-store → 必须重新完整下载客户端也可以主动提要求(注意:客户端的Cache-Control只约束客户端自己,不约束服务端):
# 客户端强制"必须去服务端校验",即便服务端说 max-age=30curl-s-D--o/dev/null-H"Cache-Control: no-cache"\http://127.0.0.1/cache/maxage|grep-i"cache-control"真实输出:
HTTP/1.1 200 OK Cache-Control: max-age=30, public # ← 服务端头不变,客户端自行决定去校验这点很关键:服务端下发的
Cache-Control是给缓存的"建议",客户端请求里的Cache-Control才是"我现在的要求"。想强制刷新,前端/代理用Cache-Control: no-cache或Cache-Control: max-age=0即可。
6. 五、重定向对 POST 的差异:301/302/303/307/308 到底差在哪
这是最容易翻车的地方。我们用一个会把 POST 包体原样回显的接口/form做重定向目标,这样能直接看到"方法有没有变、包体有没有丢"。
nginx 配置(注意目标指向/form):
location = /redir/301 { return 301 /form; } location = /redir/302 { return 302 /form; } location = /redir/303 { return 303 /form; } location = /redir/307 { return 307 /form; } location = /redir/308 { return 308 /form; }用curl -L跟随重定向,发送POST并带包体payload=1:
forcodein301302303307308;doecho"=== POST /redir/$code==="curl-s-L-XPOST-d"payload=1"http://127.0.0.1/redir/$codeechodone真实输出(服务端回显收到的包体):
=== POST /redir/301 === --- received body --- --- content-type --- === POST /redir/302 === --- received body --- --- content-type --- === POST /redir/303 === --- received body --- --- content-type --- === POST /redir/307 === --- received body --- payload=1 --- content-type --- application/x-www-form-urlencoded === POST /redir/308 === --- received body --- payload=1 --- content-type --- application/x-www-form-urlencoded结论一目了然:
301 / 302 / 303 → POST 被改成 GET,包体 payload=1 丢失(回显为空) 307 / 308 → POST 方法被保留,包体完整送达(回显 payload=1)底层原因(之前curl -v抓到的关键日志):
* Switch from POST to GET ← 301/302 默认把 POST 转 GET * Re-using existing connection with host 127.0.0.1 > POST /form HTTP/1.1 ← 307/308 仍是 POST,且带 Content-Length > Content-Length: 9语义对照表:
| 状态码 | 语义 | 对 POST 的默认行为 | 典型用途 |
|---|---|---|---|
301 | 永久移动 | 改为 GET(多数客户端) | 站点永久换域名/路径 |
302 | 临时移动 | 改为 GET(多数客户端) | 临时跳转 |
303 | 见其它 | 强制改 GET | 提交后跳结果页,防刷新重复提交 |
307 | 临时移动 | 保留原方法 | 临时跳转且需保留 POST |
308 | 永久移动 | 保留原方法 | 永久换址且需保留 POST |
关键提醒:
301/302把POST变GET是历史浏览器行为,并非规范强制;curl用-L也遵循此默认(可用--post301/--post302改变)。若你的接口需要重定向后仍是 POST(如支付回调),务必用307或308,否则参数会丢——这正是引言里那个坑。
7. 六、DNS 解析:域名是怎么变成 IP 的
HTTP 请求的第一步其实是 DNS。我们用真实命令走一遍。
7.1 本地 hosts 优先
echo"127.0.0.1 mylocal.test">>/etc/hosts getent hosts mylocal.testcurl-s-o/dev/null-w'mylocal.test -> %{http_code}\n'http://mylocal.test/真实输出:
127.0.0.1 mylocal.test mylocal.test -> 200解析顺序(Linux 由/etc/nsswitch.conf的hosts:决定,通常files dns,即先查/etc/hosts,再查 DNS)。
7.2 A 记录与 CNAME 记录
dig+short A example.comdig+short CNAME www.microsoft.com真实输出:
104.20.23.154 172.66.147.243 www.microsoft.com-c-3.edgekey.net.A记录:域名 → IPv4 地址。CNAME记录:别名 → 另一个域名(如www.microsoft.com是edgekey.net的别名,便于 CDN 调度)。
7.3dig +trace:完整迭代解析过程
dig+trace +noall +answer example.com真实输出(节选,已标注角色):
. 123997 IN NS a.root-servers.net. # ① 根服务器(.) ...(共 13 个根 NS) ;; Received 239 bytes from 127.0.0.53#53 # 本地解析器 ;; Received 1171 bytes from 198.97.190.53#53 # ② 根服务器返回 .com 的 gTLD 服务器 ;; Received 506 bytes from 192.52.178.30#53 # ③ gTLD 返回 example.com 的权威服务器 example.com. 300 IN A 172.66.147.243 # ④ 权威服务器给出最终 A 记录完整链路 ASCII 图:
浏览器/应用 │ ① 查询 根(.) → 返回 .com 的 gTLD 服务器地址 │ ② 查询 gTLD(.com) → 返回 example.com 的权威 DNS 地址 │ ③ 查询 权威服务器 → 返回 A 记录 172.66.147.243 ▼ 拿到 IP,HTTP 才真正开始建连生产提示:DNS 解析失败或污染会直接导致"连接超时"而非"连接拒绝";
dig +trace是定位解析链路问题的利器。TTL 决定记录缓存时长,改 DNS 后全球生效时间 ≈ 原 TTL。
8. 生产实践建议(Checklist)
- 认证 Cookie 必加
HttpOnly+Secure+SameSite:HttpOnly防 XSS 窃取;跨站带凭证用SameSite=None; Secure。 - CORS 做白名单而非无脑回
*:校验Origin,命中才返回Access-Control-Allow-Origin;带凭证时不要与通配符混用;别忘了放行OPTIONS预检。 - 静态资源配
ETag/Last-Modified+Cache-Control: max-age,让未变更资源走304,省带宽。 - 缓存分级:HTML 用
no-cache(每次校验),带指纹的 JS/CSS 用max-age=31536000, public(强缓存)。 - 重定向选码要看方法:需保留
POST用307/308;提交后跳转结果页用303防重复提交。 - 上线前用
curl -v -H "Origin: ..."自测 CORS、用dig +trace验证 DNS。
9. 总结
本篇用真实报文把四个"前端暗战"讲透了:
- Cookie:
Set-Cookie通过Max-Age/HttpOnly/Secure/SameSite控制生命周期与安全边界;curl -c/-b让你像浏览器一样存/带。 - 同源与 CORS:同源看协议+域名+端口;跨域靠
Access-Control-Allow-Origin等头授权,非简单请求先走OPTIONS预检。 - 条件请求:
Last-Modified/ETag+If-Modified-Since/If-None-Match,没变就304,不传包体。 - 缓存:
max-age/public/private/no-cache/no-store各司其职;新鲜度由max-age计算,客户端可主动no-cache强制校验。 - 重定向语义:
301/302/303默认把POST转GET丢包体,307/308保留方法——选错码就是线上事故。 - DNS:
/etc/hosts优先于 DNS;解析链路是 根 → gTLD → 权威,dig +trace可全程观测。
下一篇我们进入WebSocket:看它如何在一次 HTTP 握手上升级成全双工长连接,并逐字节拆解帧格式与掩码机制。
实验服务器公网 IP:120.46.193.105。本文命令均在该机真实执行,响应头/状态码/解析结果均为原始输出,未伪造。