登录态、跨域与缓存的暗战:Cookie / 同源策略 / 缓存实战

登录态、跨域与缓存的暗战:Cookie / 同源策略 / 缓存实战
登录态、跨域与缓存的暗战Cookie / 同源策略 / 缓存实战实验环境Ubuntu 24.04、nginx 1.24.0、curl 8.5.0、dnsutilsdig服务器公网 IP120.46.193.105跨域演示用同机两个端口8080API与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: sidabc123; Max-Age3600; HttpOnly; SameSiteLax; Path/ Set-Cookie: themedark; Max-Age60 cookie set再看一个更严格的 CookieSecureSameSiteStrictcurl-svhttp://127.0.0.1/setcookie_secure真实输出 Set-Cookie: tokenxyz; Secure; HttpOnly; SameSiteStrict; Max-Age120属性逐条解读属性作用生产建议Max-Age3600Cookie 存活秒数相对值另有Expires绝对时间会话型用短时效记住我功能才用长时效HttpOnly禁止 JS 通过document.cookie读取防 XSS 偷 cookie必须给认证 cookie 加Secure仅通过 HTTPS 传输明文 HTTP 下带Secure的 cookie 不会被发送SameSiteLax/Strict/None控制跨站请求是否带 cookie现代浏览器默认Lax跨站带凭证必须None且配合SecurePath/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: themedark; sidabc123 # ← 浏览器/curl 自动回传 006f:浏览器行为小结1. 响应里出现 Set-Cookie → 浏览器按属性存起来 2. 之后同域请求 → 自动在 Cookie 头里带上无需 JS 参与 3. HttpOnly 的 cookie → JS 读不到但请求照常带安全 4. Secure 的 cookie → 非 HTTPS 页面不会发送 5. SameSiteStrict → 跨站含跨域表单提交请求不带抗 CSRF3. 二、同源策略与 CORS浏览器为什么拦你同源策略Same-Origin Policy浏览器禁止页面对不同源的资源做受保护读写。协议://域名:端口三者完全一致才同源。CORSCross-Origin Resource Sharing是服务端用一组响应头显式授权哪些外源可以访问。3.1 简单请求带Origin头服务端回Access-Control-Allow-Origincurl-s-D--o/dev/null-HOrigin: 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 预检请求PreflightOPTIONS先探路当请求不简单比如带自定义头X-Custom、或用PUT/DELETE、或Content-Type: application/json浏览器会先发一个OPTIONS预检问服务端我能不能这么发得到许可后才发真正的请求。curl-s-i-XOPTIONS\-HOrigin: http://120.46.193.105:8081\-HAccess-Control-Request-Method: POST\-HAccess-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 秒期间不再预检预检通过后再发真正的POSTcurl-s-i-XPOST-HOrigin: http://120.46.193.105:8081\-HX-Custom: 1-dkvhttp://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 与 ETagcurl-s-D--o/dev/null http://127.0.0.1/cond|grep-ietag\|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-wstatus%{http_code}\n\-HIf-None-Match: 6a6440b5-63http://127.0.0.1/cond# 用 Last-Modified 比对curl-s-o/dev/null-wstatus%{http_code}\n\-HIf-Modified-Since: Sat, 25 Jul 2026 04:51:01 GMThttp://127.0.0.1/cond真实输出status304 status304两者都返回304响应没有包体浏览器直接复用本地缓存省下带宽与延迟。4.3 校验器不匹配 → 200返回新内容curl-s-o/dev/null-wstatus%{http_code}\n\-HIf-None-Match: deadbeefhttp://127.0.0.1/cond真实输出status200ETag 对不上服务端知道内容变了于是正常返回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-icache-control\|HTTPdone真实输出----- /cache/maxage ----- HTTP/1.1 200 OK Cache-Control: max-age30, 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-age605.2 指令语义指令含义是否缓存max-age30缓存 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-age30curl-s-D--o/dev/null-HCache-Control: no-cache\http://127.0.0.1/cache/maxage|grep-icache-control真实输出HTTP/1.1 200 OK Cache-Control: max-age30, public # ← 服务端头不变客户端自行决定去校验这点很关键服务端下发的Cache-Control是给缓存的建议客户端请求里的Cache-Control才是我现在的要求。想强制刷新前端/代理用Cache-Control: no-cache或Cache-Control: max-age0即可。6. 五、重定向对 POST 的差异301/302/303/307/308 到底差在哪这是最容易翻车的地方。我们用一个会把 POST 包体原样回显的接口/form做重定向目标这样能直接看到方法有没有变、包体有没有丢。nginx 配置注意目标指向/formlocation /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并带包体payload1forcodein301302303307308;doecho POST /redir/$codecurl-s-L-XPOST-dpayload1http://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 --- payload1 --- content-type --- application/x-www-form-urlencoded POST /redir/308 --- received body --- payload1 --- content-type --- application/x-www-form-urlencoded结论一目了然301 / 302 / 303 → POST 被改成 GET包体 payload1 丢失回显为空 307 / 308 → POST 方法被保留包体完整送达回显 payload1底层原因之前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临时移动保留原方法临时跳转且需保留 POST308永久移动保留原方法永久换址且需保留 POST关键提醒301/302把POST变GET是历史浏览器行为并非规范强制curl用-L也遵循此默认可用--post301/--post302改变。若你的接口需要重定向后仍是 POST如支付回调务必用307或308否则参数会丢——这正是引言里那个坑。7. 六、DNS 解析域名是怎么变成 IP 的HTTP 请求的第一步其实是 DNS。我们用真实命令走一遍。7.1 本地 hosts 优先echo127.0.0.1 mylocal.test/etc/hosts getent hosts mylocal.testcurl-s-o/dev/null-wmylocal.test - %{http_code}\nhttp://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 记录digshort A example.comdigshort 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完整迭代解析过程digtrace 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 ▼ 拿到 IPHTTP 才真正开始建连生产提示DNS 解析失败或污染会直接导致连接超时而非连接拒绝dig trace是定位解析链路问题的利器。TTL 决定记录缓存时长改 DNS 后全球生效时间 ≈ 原 TTL。8. 生产实践建议Checklist认证 Cookie 必加HttpOnlySecureSameSiteHttpOnly防 XSS 窃取跨站带凭证用SameSiteNone; Secure。CORS 做白名单而非无脑回*校验Origin命中才返回Access-Control-Allow-Origin带凭证时不要与通配符混用别忘了放行OPTIONS预检。静态资源配ETag/Last-ModifiedCache-Control: max-age让未变更资源走304省带宽。缓存分级HTML 用no-cache每次校验带指纹的 JS/CSS 用max-age31536000, public强缓存。重定向选码要看方法需保留POST用307/308提交后跳转结果页用303防重复提交。上线前用curl -v -H Origin: ...自测 CORS、用dig trace验证 DNS。9. 总结本篇用真实报文把四个前端暗战讲透了CookieSet-Cookie通过Max-Age/HttpOnly/Secure/SameSite控制生命周期与安全边界curl -c/-b让你像浏览器一样存/带。同源与 CORS同源看协议域名端口跨域靠Access-Control-Allow-Origin等头授权非简单请求先走OPTIONS预检。条件请求Last-Modified/ETagIf-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 握手上升级成全双工长连接并逐字节拆解帧格式与掩码机制。实验服务器公网 IP120.46.193.105。本文命令均在该机真实执行响应头/状态码/解析结果均为原始输出未伪造。