Appearance
Day4 HTTP 协议、浏览器缓存、跨域
📖 基础知识讲解【基础原理 + 高级深挖知识点】
基础原理
1. HTTP 报文结构(能默写才算真懂)
text
┌── 请求报文 ─────────────────────────┐ ┌── 响应报文 ─────────────────────────┐
│ GET /api/user?id=1 HTTP/1.1 │ │ HTTP/1.1 200 OK │ 起始行
│ Host: api.example.com │ │ Content-Type: application/json │
│ Cookie: token=abc │ │ Cache-Control: max-age=31536000 │ 头部
│ If-None-Match: "a1b2c3" │ │ ETag: "a1b2c3" │
│ │ │ │ 空行
│ {"name":"jack"} │ │ {"code":0,"data":{}} │ 实体
└─────────────────────────────────────┘ └─────────────────────────────────────┘核心特征: 无状态(靠 Cookie / Token 维持会话)、无连接(1.1 起默认长连接)、基于请求 - 响应模型、明文传输(HTTPS 加一层 TLS)。
2. 常用方法与状态码(面试高频快查)
| 方法 | 语义 | 幂等 | 安全 | 可缓存 |
|---|---|---|---|---|
GET | 获取资源 | ✅ | ✅ | ✅ |
POST | 提交 / 新建 | ❌ | ❌ | ❌(除非显式允许) |
PUT | 全量更新 | ✅ | ❌ | ❌ |
PATCH | 局部更新 | ❌ | ❌ | ❌ |
DELETE | 删除 | ✅ | ❌ | ❌ |
HEAD | 只要响应头 | ✅ | ✅ | ✅ |
OPTIONS | 探测能力(CORS 预检) | ✅ | ✅ | ❌ |
| 状态码 | 含义 | 关键细节 |
|---|---|---|
| 200 | 成功 | 命中强缓存时也是 200,但标注 from disk/memory cache |
| 206 | 部分内容 | 配合 Range,大文件分片下载 / 断点续传 |
| 301 | 永久重定向 | 浏览器会强缓存跳转结果,慎用(改回来很难清) |
| 302 | 临时重定向 | 可能把 POST 改成 GET,严谨场景用 307 |
| 304 | 未修改 | 协商缓存命中,不返回 body,仅返回头 |
| 307 / 308 | 临时 / 永久重定向 | 严格保持原请求方法与 body |
| 400 / 401 / 403 / 404 | 参数错 / 未认证 / 无权限 / 不存在 | 401 要重新登录,403 是已登录但没权限 |
| 413 / 414 | 体过大 / URL 过长 | Nginx client_max_body_size 常见报错 |
| 429 | 请求过多 | 限流,配合 Retry-After |
| 500 / 502 / 503 / 504 | 服务端错 / 网关错 / 不可用 / 网关超时 | 502 多为上游挂了,504 多为上游慢 |
3. HTTP 版本差异(必背主线)
| 维度 | HTTP/1.0 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|---|
| 传输层 | TCP | TCP | TCP | QUIC(UDP) |
| 连接 | 短连接 | 长连接 keep-alive、管线化 | 单连接多路复用 | 单连接多路复用 |
| 数据格式 | 文本 | 文本 | 二进制分帧 | 二进制分帧 |
| 头部 | 不压缩 | 不压缩 | HPACK 压缩 | QPACK 压缩 |
| 队头阻塞 | 有 | 应用层有(串行响应) | 应用层解决,TCP 层仍有 | 彻底消除 |
| 握手 RTT | TCP 3 次 | TCP 3 次(+TLS) | TCP + TLS | 1-RTT / 0-RTT |
| 其他 | — | Host 头、分块传输、Range | 服务端推送(已基本废弃)、流优先级 | 连接迁移(切网不断连) |
一句话串起来:1.1 省连接、2 省请求轮次与头部体积、3 省握手与丢包代价。
4. 浏览器缓存:强缓存 vs 协商缓存
| 维度 | 强缓存 | 协商缓存 |
|---|---|---|
| 是否发请求 | 不发(直接读本地) | 发(带校验头去问服务端) |
| 响应状态码 | 200 (from disk/memory cache) | 304 Not Modified(命中)/ 200(未命中) |
| 相关头 | Cache-Control: max-age、Expires | ETag/If-None-Match、Last-Modified/If-Modified-Since |
| 优先级 | 先走强缓存 | 强缓存失效后才走 |
| 适用 | 带 hash 的 js / css / 图片 / 字体 | html、接口数据、文件名不变的资源 |
完整决策链路(面试默画):
text
发起请求
│
├─ 有本地缓存?── 否 ─────────────────────────────> 直接请求服务端 → 200
│ │是
│ ▼
│ 强缓存是否过期?(Cache-Control:max-age 优先于 Expires)
│ │
│ ├─ 未过期 ──> 直接用本地副本 200 (from memory/disk cache) 【0 请求】
│ │
│ └─ 已过期 ──> 带 If-None-Match(ETag) / If-Modified-Since 请求
│ │
│ ├─ 资源未变 ──> 304,用本地副本并刷新缓存头
│ └─ 资源已变 ──> 200 + 新 body + 新 ETag5. 缓存存放在哪(四级缓存,按查找顺序)
| 位置 | 特点 | 典型场景 |
|---|---|---|
| Service Worker | 开发者完全可控,可拦截请求自定义缓存策略 | PWA、离线可用 |
| Memory Cache | 内存,读取最快,Tab 关闭即失效 | 当前页重复引用的图片、脚本 |
| Disk Cache | 磁盘,容量大、可跨会话持久 | 绝大多数静态资源 |
| Push Cache | HTTP/2 服务端推送专用,只在会话期内有效 | 已基本不用 |
小文件和当前页正在使用的资源倾向进内存,大文件进磁盘;强制刷新(Ctrl+F5)会带
Cache-Control: no-cache跳过强缓存,普通刷新(F5)通常只跳过内存缓存。
6. 同源策略与跨域
同源 = 协议 + 域名 + 端口 三者完全一致,任一不同即跨域(注意:http://a.com 与 https://a.com 跨域,a.com 与 www.a.com 跨域,IP 与域名跨域)。
同源策略限制的三类行为:
- AJAX / Fetch 请求:请求其实已经发出,是浏览器把响应拦截了(所以后端能收到日志);
- DOM 访问:跨域 iframe / window 之间不能互相读 DOM;
- 存储读取:Cookie、LocalStorage、IndexedDB 按源隔离。
不受同源策略限制的标签:
<img>、<script>、<link>、<iframe>、<video>—— JSONP 正是利用了<script>的这个特性。
跨域方案全景对比:
| 方案 | 原理 | 适用 / 限制 |
|---|---|---|
| CORS | 服务端返回 Access-Control-Allow-* 响应头 | 主流首选,支持所有方法,需后端配合 |
| Nginx 反向代理 | 同源域名转发到真实后端,服务端之间无跨域 | 生产常用,前端零改造 |
| devServer proxy | 本地 Node 服务代理转发 | 仅开发环境 |
| JSONP | <script> 加载 + 全局回调 | 仅 GET、有 XSS 风险,兼容老浏览器才用 |
| postMessage | 窗口 / iframe 间消息通信 | 跨窗口通信,必须校验 origin |
| WebSocket | 协议本身不受同源策略限制 | 实时通信,服务端需校验 Origin |
| document.domain | 主域相同的子域互信(已废弃) | 仅同主域,Chrome 115+ 已默认禁用 |
| window.name / hash | 借助窗口属性或 URL hash 传值 | 古早方案,了解即可 |
✨ 高级深挖知识点
1. Cache-Control 指令全表(加分项密集区)
| 指令 | 作用 | 使用场景 |
|---|---|---|
max-age=31536000 | 资源在客户端的新鲜期(秒),优先级高于 Expires | 带 hash 的静态资源(一年) |
s-maxage=600 | 只对代理 / CDN 生效的新鲜期,覆盖 max-age | CDN 缓存 html 短期 |
no-cache | 允许缓存但每次必须协商校验(≠ 不缓存) | html 入口文件 |
no-store | 完全不缓存,连磁盘都不写 | 支付、含敏感数据的接口 |
private | 只允许浏览器缓存,代理 / CDN 不得缓存 | 含用户信息的响应 |
public | 浏览器与代理都可缓存 | 公共静态资源 |
must-revalidate | 过期后必须向源站校验,不允许直接用过期副本 | 对一致性要求高的资源 |
immutable | 新鲜期内即便用户刷新也不发协商请求 | 内容哈希资源,配合 max-age |
stale-while-revalidate=60 | 过期后 60s 内先返回旧副本,后台异步静默更新 | 体验优先的接口 / 页面(SWR) |
stale-if-error=86400 | 源站报错时允许继续用过期副本兜底 | 容灾降级 |
最佳实践组合:
text
hash 静态资源 : Cache-Control: public, max-age=31536000, immutable
html 入口 : Cache-Control: no-cache (或 max-age=0, must-revalidate)
用户私有接口 : Cache-Control: private, no-store
CDN 分发页面 : Cache-Control: max-age=0, s-maxage=600, stale-while-revalidate=60
Expires是 HTTP/1.0 遗产,值为绝对时间,受客户端系统时间影响(改本地时间就失效);Cache-Control: max-age是相对时间,更可靠且优先级更高。
2. ETag vs Last-Modified 深挖
| 维度 | Last-Modified / If-Modified-Since | ETag / If-None-Match |
|---|---|---|
| 本质 | 资源最后修改时间 | 资源内容指纹(hash 或 inode+size+mtime) |
| 精度 | 秒级,1 秒内多次修改感知不到 | 内容级,改一个字节就变 |
| 优先级 | 低 | 高(两者都在时服务端先校验 ETag) |
| 弱点 | 内容未变但时间变(如重新部署)会误判为更新 | 计算有 CPU 成本;分布式多机需保证一致 |
| 强弱 | — | 强校验 "abc" / 弱校验 W/"abc"(语义等价即可命中) |
典型工程坑:
- 多机部署 ETag 不一致:Nginx 默认 ETag =
mtime + size,不同机器文件时间戳不同 → 同一资源在 A 机命中、B 机 200,缓存命中率骤降。解法:统一用内容 hash 生成 ETag,或构建产物同步时保留 mtime。 - Gzip 改变 ETag:开启压缩后部分服务器会修改或丢弃 ETag,需确认网关行为。
- 只有
Last-Modified时,服务端用If-Modified-Since <= mtime判断,所以「文件回滚成旧版本」会因时间变新而被判为更新。
3. CORS 完整机制
简单请求(不触发预检)必须同时满足:
- 方法是
GET/POST/HEAD; - 头部只在白名单内(
Accept、Accept-Language、Content-Language、Content-Type、Range等); Content-Type只能是text/plain、multipart/form-data、application/x-www-form-urlencoded。
所以只要你发
application/json,就一定会触发 OPTIONS 预检 —— 这是最高频的面试追问点。
预检流程(非简单请求):
text
浏览器 服务端
│ OPTIONS /api/user │
│ Origin: https://web.a.com │
│ Access-Control-Request-Method: PUT │
│ Access-Control-Request-Headers: content-type,authorization
│ ─────────────────────────────────────────────> │
│ │ 校验 Origin / Method / Headers
│ 204 No Content │
│ Access-Control-Allow-Origin: https://web.a.com
│ Access-Control-Allow-Methods: GET,POST,PUT │
│ Access-Control-Allow-Headers: content-type,authorization
│ Access-Control-Allow-Credentials: true │
│ Access-Control-Max-Age: 86400 ← 预检结果缓存一天
│ <───────────────────────────────────────────── │
│ │
│ PUT /api/user (真实业务请求) │
│ ─────────────────────────────────────────────> │CORS 响应头速查:
| 响应头 | 作用 |
|---|---|
Access-Control-Allow-Origin | 允许的源,* 或具体域名(带凭证时不能用 *) |
Access-Control-Allow-Methods | 预检放行的方法 |
Access-Control-Allow-Headers | 预检放行的自定义请求头 |
Access-Control-Allow-Credentials | 是否允许携带 Cookie,需前端同时设 withCredentials / credentials:'include' |
Access-Control-Expose-Headers | 前端能读到的响应头白名单(默认只能读 6 个基础头) |
Access-Control-Max-Age | 预检结果缓存秒数(Chrome 上限 7200s) |
Vary: Origin | 动态回显 Origin 时必加,否则 CDN 会把 A 域的响应发给 B 域 |
带凭证跨域三件套(缺一不可):
- 后端
Access-Control-Allow-Origin为具体域名(不可*); - 后端
Access-Control-Allow-Credentials: true; - 前端
xhr.withCredentials = true或fetch(url, { credentials: 'include' }); - Cookie 本身要满足
SameSite=None; Secure(Chrome 80+)。
4. Cookie 与 SameSite(跨域登录态的真正拦路虎)
SameSite 值 | 行为 | 影响 |
|---|---|---|
Strict | 完全禁止第三方上下文携带 | 最安全,但从外链跳回来会丢登录态 |
Lax(默认) | 只有**顶级导航的安全方法(GET)**才携带 | 跨域 XHR / POST / iframe 不带 Cookie |
None | 任何跨站请求都携带,必须同时加 Secure(HTTPS) | 跨域鉴权唯一可行选项 |
Chrome 80 起默认
Lax,这是「本地联调能登录、换域名就掉登录」的根因。规避思路:① 统一主域 + Cookie 设Domain=.a.com;② Nginx 代理成同源;③ 改用Authorization: Bearer <token>头部鉴权,彻底绕开 Cookie。
5. HTTPS 与 TLS 握手
text
TCP 三次握手 (1 RTT)
│
▼
TLS 握手
① ClientHello → 支持的 TLS 版本、加密套件列表、Client Random
② ServerHello ← 选定套件、Server Random、证书链(公钥)
③ 客户端校验证书(CA 信任链、域名、有效期、吊销状态)
④ 密钥交换(RSA 或 ECDHE,ECDHE 具备前向安全性)→ 生成会话密钥
⑤ Finished 双向确认 → 之后全部用对称加密通信- 非对称加密只用来安全交换对称密钥,之后数据用对称加密(AES-GCM),因为对称加密快得多。
- TLS 1.2 需 2-RTT,TLS 1.3 只需 1-RTT,会话复用(Session Ticket / PSK)可做到 0-RTT。
- 中间人攻击的防线是证书校验:抓包工具(Charles / Whistle)之所以能解 HTTPS,是因为你手动信任了它的根证书;移动端可用 SSL Pinning(证书 / 公钥固定)防护。
Strict-Transport-Security(HSTS)强制浏览器后续只用 HTTPS,避免首次 http 跳转被劫持。
6. 队头阻塞的三层真相
| 层级 | 现象 | 谁能解决 |
|---|---|---|
| HTTP/1.1 应用层 | 同一连接请求必须按序返回,前一个慢全部排队;浏览器只好开 6 个并发连接 | HTTP/2 多路复用 |
| TCP 传输层 | 任一包丢失,整个连接的字节流都要等重传(TCP 保序) | 只有 QUIC(流级独立重传) |
| 浏览器连接数 | 同域最多 6 个 TCP 连接 | 域名分片(HTTP/1.1 时代)/ HTTP/2 |
HTTP/2 反而在弱网更差的经典结论:多路复用把所有流塞进一个 TCP 连接,一旦丢包,所有流一起卡;而 HTTP/1.1 的 6 条连接里只有 1 条受影响。QUIC 在 UDP 之上自己实现流控与重传,流之间互不影响,这才是 HTTP/3 的核心价值。
7. HTTP/2 细节补充
- 二进制分帧层:一个 TCP 连接上有多个 Stream,每个 Stream 由
HEADERS/DATA等 Frame 组成,帧带 Stream ID,可乱序发送、接收端按 ID 重组。 - HPACK:静态表(61 个常见头)+ 动态表 + 哈夫曼编码,重复请求头几乎零开销(1.1 下每个请求重复发几百字节 Cookie/UA)。
- 服务端推送(Server Push)已被 Chrome 106 移除,面试提一句「现在用
103 Early Hints+preload替代」是加分项。 - HTTP/2 下域名分片、雪碧图、资源内联这些 1.1 优化反成负担(破坏缓存粒度、增加体积)。
8. 代理与 CDN 缓存
Vary: Accept-Encoding, Origin, User-Agent:告诉缓存服务器按哪些请求头区分缓存副本;Vary: *等于禁用缓存;滥用User-Agent会让命中率崩盘。- CDN 回源与缓存分层:边缘节点 → 区域节点 → 源站;通过
s-maxage控制边缘 TTL,发布后用 主动刷新(purge)/ 预热 让新版本生效。 - 缓存击穿与版本控制:永远不要靠「清缓存」来发布,而是靠 文件名 hash(内容指纹) 天然失效;html 用
no-cache保证入口实时。
❓ 高频面试题【基础简答 + 连环追问链 + 精简口述答案 + 项目关联话术】
题 1:强缓存与协商缓存的区别?Cache-Control / Expires、ETag / Last-Modified 优先级?
精简口述答案:
强缓存不发请求,直接读本地副本,状态码是 200 并标注
from memory/disk cache,由Cache-Control: max-age和Expires控制;协商缓存会发请求但不传 body,带上If-None-Match/If-Modified-Since让服务端校验,资源没变就返回 304,浏览器继续用本地副本。浏览器先判强缓存,失效后才走协商缓存。优先级上:Cache-Control高于Expires(后者是绝对时间,受本地时钟影响);ETag高于Last-Modified(前者是内容指纹,后者只有秒级精度)。
连环追问链:
- 追问:
no-cache和no-store的区别?- 答:
no-cache是「可以缓存,但每次用之前必须找服务端校验」,走的是协商缓存,能命中 304;no-store是「完全不准存」,每次都完整重新下载,用于支付、隐私类响应。
- 答:
- 追问:
max-age=0和no-cache等价吗?- 答:基本等价(都会去校验),但严格说
max-age=0允许某些代理在配置下返回过期副本,加上must-revalidate才完全等价于no-cache。
- 答:基本等价(都会去校验),但严格说
- 追问:
stale-while-revalidate的作用?- 答:资源过期后的宽限期内,先把旧副本立刻返回给用户,同时后台异步发请求更新缓存。用户感知是「秒开」,代价是可能看到一次旧数据,非常适合对实时性要求不极致的页面和接口。
- 追问:为什么静态资源能设一年强缓存却不怕更新不了?
- 答:因为文件名里带内容 hash,内容一变文件名就变,等于换了一个新 URL,老缓存自然作废;真正不能强缓存的是 html 入口,它负责引用最新的 hash 文件名。
- 追问:普通刷新(F5)、强制刷新(Ctrl+F5)、地址栏回车三者的缓存行为差异?
- 答:地址栏回车 → 强缓存、协商缓存都可能命中;F5 → 跳过内存缓存,一般带
max-age=0走协商校验;Ctrl+F5 → 带Cache-Control: no-cache+Pragma: no-cache,强缓存协商缓存全跳过,直接 200 重新下载。
- 答:地址栏回车 → 强缓存、协商缓存都可能命中;F5 → 跳过内存缓存,一般带
- 追问:接口数据能不能用 HTTP 缓存?
- 答:GET 幂等接口可以用
ETag+no-cache做协商缓存,省带宽;含用户私有数据的必须加private,敏感接口直接no-store。实际项目里更常用前端内存缓存 / SWR 方案,可控性更强。
- 答:GET 幂等接口可以用
✅ 项目关联话术(按真实经历替换):
项目构建产物(js / css / 图片 / 字体)文件名统一带 contenthash,Nginx 下发
Cache-Control: public, max-age=31536000, immutable;index.html固定no-cache,保证发版后用户刷新就能拿到新的 hash 引用。上线后静态资源缓存命中率约 90%,二次打开首屏从 1.6s 降到 0.5s 左右。另外踩过一次坑:多台机器 Nginx 默认按 mtime 生成 ETag 导致同一文件在不同机器 ETag 不同,命中率异常,后来统一由构建产物 hash 生成才解决。
题 2:什么是跨域?同源策略限制哪些场景?CORS 的完整流程?
精简口述答案:
同源策略是浏览器最核心的安全机制,协议、域名、端口三者完全一致才算同源,任一不同就是跨域。它限制三类行为:AJAX / Fetch 的响应读取、跨域 DOM 访问、以及 Cookie / Storage 的读取。注意跨域请求其实已经发到服务端了,只是响应被浏览器拦住不给 JS 读。CORS 分两种:简单请求直接发出去,浏览器根据响应里的
Access-Control-Allow-Origin决定放不放行;非简单请求(比如application/json的 POST、PUT、带自定义头)浏览器会先发一个OPTIONS预检请求,服务端确认方法和请求头都允许后,才发真正的业务请求。
连环追问链:
- 追问:什么情况下会触发预检?怎么避免?
- 答:方法不是 GET/POST/HEAD、带了自定义头(如
Authorization、X-Token)、或Content-Type不是那三种白名单类型(最常见的就是application/json)。想避免可以改用表单类型提交,但一般不值得,更合理的是配Access-Control-Max-Age缓存预检结果。
- 答:方法不是 GET/POST/HEAD、带了自定义头(如
- 追问:跨域携带 Cookie 要注意什么?
- 答:后端
Access-Control-Allow-Origin必须是具体域名不能是*,并返回Access-Control-Allow-Credentials: true,前端要开withCredentials/credentials: 'include',同时 Cookie 本身要SameSite=None; Secure,四个条件缺一不可。
- 答:后端
- 追问:前端为什么读不到自定义响应头(比如分页总数)?
- 答:默认只能读 6 个基础头,自定义头要后端通过
Access-Control-Expose-Headers: X-Total-Count显式暴露。
- 答:默认只能读 6 个基础头,自定义头要后端通过
- 追问:JSONP 的优缺点?
- 答:优点是利用
<script>天然跨域,兼容性极好;缺点是只支持 GET、无法拿到真实状态码、错误处理困难,而且要执行对方返回的脚本,被劫持就等于 XSS,现在只在对接老系统时才用。
- 答:优点是利用
- 追问:Nginx 反向代理为什么能解决跨域?
- 答:跨域是浏览器的限制,服务端之间请求没有同源策略。把接口代理到和页面相同的域名下,浏览器看到的就是同源请求,自然不拦。
- 追问:服务端配了
Access-Control-Allow-Origin: *还报跨域,可能是什么原因?- 答:常见三种:① 请求带了凭证,
*对带凭证的请求无效;② 预检OPTIONS被网关 / 鉴权中间件拦截返回了 401 或 404,压根没带上 CORS 头;③ 重定向后的响应没有 CORS 头;④ 头被下发了两次(Nginx 和后端都加),浏览器判定非法。
- 答:常见三种:① 请求带了凭证,
- 追问:CORS 能防住 CSRF 吗?
- 答:不能完全防。CORS 只管「能不能读响应」,表单提交、
<img>这类简单请求照样能打到服务端造成写操作。防 CSRF 要靠 CSRF Token、SameSiteCookie、校验Origin/Referer。
- 答:不能完全防。CORS 只管「能不能读响应」,表单提交、
✅ 项目关联话术(按真实经历替换):
前后端分离项目,开发环境用 Vite 的
server.proxy把/api代理到测试服务;生产环境走 Nginx 同域转发,前端完全无感知。对外开放的开放平台接口才单独用 CORS 白名单:后端按请求Origin动态回显并加上Vary: Origin,避免 CDN 把 A 域的响应错发给 B 域;同时配了Access-Control-Max-Age: 86400,OPTIONS 请求量下降了一大半。
题 3:HTTP/1.1、HTTP/2、HTTP/3 的核心差异?
精简口述答案:
HTTP/1.1 是文本协议,默认长连接,但同一连接响应必须按序返回,存在应用层队头阻塞,浏览器只能靠同域 6 个并发连接和域名分片缓解。HTTP/2 改成二进制分帧,一个 TCP 连接上多路复用多个流,配合 HPACK 头部压缩和流优先级,解决了应用层队头阻塞;但它跑在 TCP 上,一旦丢包,所有流都要等重传,TCP 层队头阻塞依然存在。HTTP/3 换成基于 UDP 的 QUIC,在用户态自己实现流控和重传,流与流之间独立,彻底消除队头阻塞,并把 TLS 握手合并进连接建立做到 1-RTT 甚至 0-RTT,还支持连接迁移,手机从 WiFi 切 4G 连接不中断。
连环追问链:
- 追问:HTTP/2 都多路复用了,为什么丢包还会阻塞所有流?
- 答:因为 TCP 对上层提供的是有序可靠的字节流,中间任何一个包丢了,后面的数据即使到达也必须在内核缓冲区里等重传完成才能交付。HTTP/2 在应用层看不到这一层,无能为力。
- 追问:QUIC 基于 UDP,怎么保证可靠性?
- 答:QUIC 在用户态重新实现了序号、确认、重传、拥塞控制和流量控制,并且是按 Stream 独立管理的,所以某个流丢包不影响其他流;同时内置 TLS 1.3,加密是强制的。
- 追问:HTTP/2 下还需要雪碧图、域名分片、资源内联吗?
- 答:不需要,甚至有害。这些都是为绕开 1.1 并发限制的 hack,在 2 下会破坏缓存粒度(改一张小图整张雪碧图失效)、域名分片还会拆散多路复用的单连接优势。
- 追问:HTTP/2 的服务端推送现在还用吗?
- 答:基本废弃了,Chrome 106 已移除。原因是难以判断客户端是否已缓存,容易推送浪费带宽;现在用
103 Early Hints+preload/preconnect替代。
- 答:基本废弃了,Chrome 106 已移除。原因是难以判断客户端是否已缓存,容易推送浪费带宽;现在用
- 追问:升级 HTTP/2 / HTTP/3 需要改前端代码吗?
- 答:不需要,是传输层协议升级,由网关 / CDN 开启即可(HTTP/2 必须 HTTPS,HTTP/3 需通过
Alt-Svc响应头告知客户端可升级)。要做的是把 1.1 时代的优化 hack 撤掉。
- 答:不需要,是传输层协议升级,由网关 / CDN 开启即可(HTTP/2 必须 HTTPS,HTTP/3 需通过
✅ 项目关联话术(按真实经历替换):
静态资源站和主站网关统一开启 HTTP/2 后,首屏几十个小资源从 6 并发排队变成单连接并行,瀑布图里的排队(Queueing)时间几乎消失,首屏资源加载总时长下降约 30%;同时把原来为绕 1.1 并发做的域名分片(img1/img2/img3)收敛成一个域名,复用同一条连接和 TLS 会话,弱网下表现反而更稳。
题 4:HTTPS 的加密流程是怎样的?为什么抓包工具能看到 HTTPS 明文?
精简口述答案:
HTTPS = HTTP + TLS。流程是先 TCP 三次握手,然后 TLS 握手:客户端发 ClientHello 带上支持的版本和加密套件,服务端回 ServerHello 和证书,客户端校验证书链(是否受信任 CA 签发、域名是否匹配、是否过期吊销),接着通过 ECDHE 等算法协商出对称会话密钥,之后所有数据用对称加密传输。非对称加密只用于安全地交换对称密钥,因为非对称运算慢。抓包工具能看明文,是因为它充当中间人,用自己的根证书重新签发了目标站点证书,而你手动在系统里信任了这张根证书 —— 所以 HTTPS 的安全底线其实是证书信任链。
连环追问链:
- 追问:为什么不全程用非对称加密?
- 答:非对称加密(RSA/ECC)运算量比对称加密(AES)大几个数量级,全程使用性能无法接受;而且对称密钥通过非对称安全交换一次即可,兼顾安全与性能。
- 追问:TLS 1.3 相比 1.2 快在哪?
- 答:1.2 需要 2-RTT,1.3 砍掉了冗余往返做到 1-RTT,会话复用场景支持 0-RTT(有重放攻击风险,只能用于幂等请求);同时废弃了 RSA 密钥交换等不安全套件,强制前向安全。
- 追问:怎么防止中间人攻击?
- 答:Web 端依赖 CA 体系 + HSTS(强制 HTTPS)+ 证书透明度;App 端可做 SSL Pinning,把服务端证书或公钥内置到客户端,不信任系统证书库,抓包工具就失效了。
- 追问:HTTPS 一定更慢吗?
- 答:首次握手确实多几个 RTT,但配合 TLS 1.3、会话复用、OCSP Stapling、HTTP/2 多路复用后,整体往往比 HTTP/1.1 更快;而且 HTTP/2、HTTP/3 本身就要求加密。
✅ 项目关联话术(按真实经历替换):
移动端 H5 调试时用 Whistle / Charles 抓 HTTPS,需要在手机上安装并信任根证书;上线前针对核心接口做了 Origin / Referer 校验与签名验签,防止被中间人改包重放。另外全站配置 HSTS,杜绝用户从 http 入口被劫持跳转。
题 5:跨域场景下登录态丢失(Cookie 带不过去)怎么排查和解决?
精简口述答案:
先分清是「没带 Cookie」还是「带了但被拒」。没带 Cookie 的典型原因是:前端没开
withCredentials、Cookie 的SameSite是默认的Lax导致跨站 XHR 不携带、Cookie 的Domain/Path不匹配、或者 HTTP 环境下设了SameSite=None却没有Secure被浏览器直接丢弃。解决路径有三条:最优是做成同源(Nginx 代理或统一主域 +Domain=.a.com共享 Cookie);其次是完整配好跨域凭证四件套(具体 Origin +Allow-Credentials+withCredentials+SameSite=None; Secure);最省事的是改用 Token 放在Authorization头里,不依赖 Cookie,天然没有 SameSite 问题,但要注意 XSS 下的存储安全。
连环追问链:
- 追问:Cookie 和 Token(localStorage)鉴权各自的安全风险?
- 答:Cookie 自动携带,易受 CSRF,需靠
SameSite+ CSRF Token 防护,但配HttpOnly能免疫 JS 窃取;Token 放 localStorage 不怕 CSRF,但 XSS 下可被直接读走。折中方案是HttpOnlyCookie 存 refresh token + 内存存 access token。
- 答:Cookie 自动携带,易受 CSRF,需靠
- 追问:主域相同、子域不同(
a.jd.com与b.jd.com)怎么共享登录态?- 答:Cookie 设
Domain=.jd.com即可共享(document.domain方案已废弃);但 AJAX 依然跨域,仍要配 CORS。
- 答:Cookie 设
- 追问:怎么判断请求到底带没带 Cookie?
- 答:看 DevTools Network 里该请求的 Request Headers 有没有
Cookie,并在 Application → Cookies 里确认SameSite/Secure/Domain字段;Chrome 还会在 Cookie 被拦截时给出黄色警告提示原因。
- 答:看 DevTools Network 里该请求的 Request Headers 有没有
✅ 项目关联话术(按真实经历替换):
管理后台曾出现「本地联调能登录、测试环境掉登录」的问题,排查发现测试环境前后端域名不同,Cookie 是默认
Lax没带上。短期方案是后端把会话 Cookie 改为SameSite=None; Secure并开启凭证跨域,长期方案是 Nginx 统一把/api代理到同域,顺便彻底消除了预检请求。
✍️ 手撕代码【核心实现 + 边界 Case 自测清单 + 变种拓展题】
题 1:原生 JS 实现 JSONP(生产级:超时 + 错误处理 + 清理)
核心实现:
js
let jsonpId = 0;
function jsonp(url, { params = {}, callbackKey = "callback", timeout = 8000 } = {}) {
return new Promise((resolve, reject) => {
// 1. 回调函数名唯一,避免并发请求互相覆盖
const callbackName = `__jsonp_cb_${Date.now()}_${jsonpId++}`;
const script = document.createElement("script");
let timer = null;
// 2. 统一清理:全局变量、script 标签、定时器,防内存泄漏
const cleanup = () => {
delete window[callbackName];
script.onerror = null;
script.parentNode && script.parentNode.removeChild(script);
timer && clearTimeout(timer);
};
// 3. 服务端返回的是 callbackName({...}) 这段可执行脚本
window[callbackName] = (data) => {
cleanup();
resolve(data);
};
// 4. 网络错误 / 404:script 的 onerror 能捕获(但拿不到状态码)
script.onerror = () => {
cleanup();
reject(new Error(`JSONP request failed: ${url}`));
};
// 5. 超时兜底:服务端不调用回调时 onerror 不会触发
if (timeout > 0) {
timer = setTimeout(() => {
cleanup();
reject(new Error(`JSONP timeout: ${url}`));
}, timeout);
}
// 6. 拼接查询串(注意编码,防止特殊字符破坏 URL)
const query = Object.entries({ ...params, [callbackKey]: callbackName })
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join("&");
script.src = `${url}${url.includes("?") ? "&" : "?"}${query}`;
document.body.appendChild(script);
});
}
// 使用
jsonp("https://api.example.com/search", { params: { kw: "前端" } })
.then((res) => console.log(res))
.catch((err) => console.error(err));✅ 边界 Case 自测:
- 并发调用:回调名必须唯一(时间戳 + 自增 id),否则后一个请求会覆盖前一个的回调。
- 清理彻底:成功、失败、超时三条路径都要
delete window[cb]并移除<script>,否则全局变量和 DOM 会不断堆积。 - 超时必须有:服务端返回了 200 但不调用回调时,
onerror不会触发,Promise 会永远挂起。 - 参数编码:
params中含中文、&、=时必须encodeURIComponent。 - 无法取消 / 拿不到状态码:这是 JSONP 的固有缺陷,面试要主动点出。
- 安全性:返回内容会被当脚本执行,只能用于完全可信的接口。
🔁 变种拓展题: 给 JSONP 加「同 URL 请求去重」和「取消」能力。
js
const pending = new Map(); // url -> promise,相同请求复用
function jsonpOnce(url, options) {
if (pending.has(url)) return pending.get(url);
const p = jsonp(url, options).finally(() => pending.delete(url));
pending.set(url, p);
return p;
}题 2:postMessage 跨窗口 / iframe 通信(带 origin 校验与请求 - 响应封装)
核心实现:
js
// ========== 父页面 ==========
const iframe = document.getElementById("child");
const CHILD_ORIGIN = "https://child.example.com";
// ✅ 第二个参数必须写明确的 targetOrigin,绝不要用 '*'(会把数据发给任何占位页面)
iframe.onload = () => {
iframe.contentWindow.postMessage({ type: "INIT", payload: { token: "abc" } }, CHILD_ORIGIN);
};
window.addEventListener("message", (e) => {
// ✅ 必须校验来源,否则任意页面都能向你发消息
if (e.origin !== CHILD_ORIGIN) return;
if (e.source !== iframe.contentWindow) return; // 进一步校验发送方窗口
console.log("收到子页面消息:", e.data);
});
// ========== 子页面(iframe 内) ==========
const PARENT_ORIGIN = "https://parent.example.com";
window.addEventListener("message", (e) => {
if (e.origin !== PARENT_ORIGIN) return; // ✅ 白名单校验
const { type, payload } = e.data || {};
if (type === "INIT") {
localStorage.setItem("token", payload.token);
// 回传消息给父页面
e.source.postMessage({ type: "INIT_DONE" }, e.origin);
}
});进阶:封装成带 id 的请求 - 响应模型(类似 RPC):
js
function createMessenger(targetWindow, targetOrigin) {
const callbacks = new Map();
let seq = 0;
window.addEventListener("message", (e) => {
if (e.origin !== targetOrigin) return; // 安全第一
const { __id, __res, data } = e.data || {};
if (__res && callbacks.has(__id)) {
callbacks.get(__id)(data); // 唤醒对应的 Promise
callbacks.delete(__id);
}
});
return function request(type, payload, timeout = 5000) {
return new Promise((resolve, reject) => {
const __id = `msg_${seq++}`;
const timer = setTimeout(() => {
callbacks.delete(__id);
reject(new Error(`postMessage timeout: ${type}`));
}, timeout);
callbacks.set(__id, (data) => {
clearTimeout(timer);
resolve(data);
});
targetWindow.postMessage({ __id, type, payload }, targetOrigin);
});
};
}✅ 边界 Case 自测:
- 必校验
e.origin(白名单),并尽量校验e.source,否则是明确的安全漏洞。 targetOrigin不要写*:目标页若被替换成恶意页面,敏感数据会直接泄露。- 时序问题:iframe 未加载完就
postMessage会丢消息,要等onload或由子页面先发READY握手。 - 数据可克隆:传参走结构化克隆算法,函数、DOM 节点、循环引用(部分场景)无法传递。
- 卸载清理:组件销毁时
removeEventListener,并清空等待中的回调与定时器。
🔁 变种拓展题: 同源多标签页通信用 BroadcastChannel 或 storage 事件;跨域且需长连接用 WebSocket;跨域共享存储可用「中间 iframe + postMessage」代理方案。
题 3:Node 实现强缓存 + 协商缓存(把缓存链路跑通一遍)
核心实现:
js
const http = require("http");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
// 用文件内容生成 ETag,保证多机一致(不要用 mtime + size)
const genETag = (buf) => `"${crypto.createHash("md5").update(buf).digest("hex")}"`;
http
.createServer((req, res) => {
const filePath = path.join(__dirname, "public", req.url === "/" ? "index.html" : req.url);
if (!fs.existsSync(filePath)) {
res.writeHead(404).end("Not Found");
return;
}
const buf = fs.readFileSync(filePath);
const stat = fs.statSync(filePath);
const etag = genETag(buf);
const lastModified = stat.mtime.toUTCString();
const isHtml = filePath.endsWith(".html");
// 1. 下发缓存策略:html 走协商缓存,hash 静态资源走一年强缓存
res.setHeader("ETag", etag);
res.setHeader("Last-Modified", lastModified);
res.setHeader(
"Cache-Control",
isHtml ? "no-cache" : "public, max-age=31536000, immutable"
);
// 2. 协商缓存校验:ETag 优先于 Last-Modified
const ifNoneMatch = req.headers["if-none-match"];
const ifModifiedSince = req.headers["if-modified-since"];
const etagMatched = ifNoneMatch && ifNoneMatch === etag;
const timeMatched = !ifNoneMatch && ifModifiedSince === lastModified;
if (etagMatched || timeMatched) {
res.writeHead(304).end(); // ✅ 304 不能带 body
return;
}
res.writeHead(200).end(buf);
})
.listen(3000);✅ 边界 Case 自测:
- 第一次请求 → 200;刷新 → html 返回 304、静态资源
from disk cache。 - 304 响应不能携带 body,否则浏览器解析异常。
ETag必须由内容生成,多机部署才一致;弱校验要写成W/"xxx"。If-None-Match可能携带多个值(逗号分隔)或*,严谨实现要拆分比对。- 同时存在两种校验头时,以
ETag为准。 - 大文件应改为
fs.createReadStream流式响应,并支持Range→206。
🔁 变种拓展题: 加上 Range 分片响应(断点续传)与 gzip / br 压缩协商(依据 Accept-Encoding,并下发 Vary: Accept-Encoding)。
题 4:CORS 中间件与 Nginx 代理配置(后端视角把跨域配对)
核心实现(Node / Express 风格中间件):
js
const WHITE_LIST = ["https://web.example.com", "https://admin.example.com"];
function cors(req, res, next) {
const origin = req.headers.origin;
// 1. 动态回显白名单内的 Origin(带凭证时不能用 *)
if (origin && WHITE_LIST.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
// 2. 动态回显必须加 Vary,否则 CDN 会把 A 域的响应发给 B 域
res.setHeader("Vary", "Origin");
}
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Request-Id");
// 3. 前端需要读的自定义响应头必须显式暴露
res.setHeader("Access-Control-Expose-Headers", "X-Total-Count,X-Request-Id");
// 4. 缓存预检结果,减少 OPTIONS 请求
res.setHeader("Access-Control-Max-Age", "86400");
// 5. 预检请求直接短路返回,绝不能进鉴权中间件(OPTIONS 不带业务凭证)
if (req.method === "OPTIONS") {
res.writeHead(204).end();
return;
}
next();
}Nginx 反向代理(生产首选,前端零改造):
nginx
server {
listen 80;
server_name web.example.com;
location / {
root /data/dist;
try_files $uri $uri/ /index.html; # SPA history 路由兜底
add_header Cache-Control "no-cache"; # 入口 html 不强缓存
}
# 带 hash 的静态资源:一年强缓存
location ~* \.(js|css|png|jpg|webp|woff2)$ {
root /data/dist;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 接口同域转发 —— 跨域问题从根上消失
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1; # 保持长连接
proxy_set_header Connection "";
}
}✅ 边界 Case 自测:
OPTIONS预检必须在鉴权之前短路,否则会被 401 拦掉导致「配了 CORS 还报跨域」。- 同一响应头不能被 Nginx 和后端各加一次,重复的
Access-Control-Allow-Origin会被浏览器判为非法。 - 动态回显 Origin 必带
Vary: Origin;白名单比对要全等,不要用includes做子串匹配(evil-example.com能绕过)。 - 重定向(301/302)后的响应同样需要 CORS 头。
- Nginx
add_header默认只在 2xx/3xx 生效,错误响应要加always。
🔁 变种拓展题: Vite / Webpack devServer 代理配置。
js
// vite.config.ts
export default {
server: {
proxy: {
"/api": {
target: "http://test-api.example.com",
changeOrigin: true, // 重写 Host 头,避免后端域名校验失败
rewrite: (p) => p.replace(/^\/api/, ""),
cookieDomainRewrite: "localhost", // Cookie 域名重写,解决本地登录态
},
},
},
};题 5:前端请求层缓存(SWR 思路 + 请求去重)
核心实现:
js
const cache = new Map(); // key -> { data, time }
const inflight = new Map(); // key -> promise,并发去重
async function requestWithCache(url, { maxAge = 30_000, swr = 300_000 } = {}) {
const key = url;
const hit = cache.get(key);
const now = Date.now();
// 1. 新鲜期内:直接返回缓存(等价强缓存)
if (hit && now - hit.time < maxAge) return hit.data;
const revalidate = () => {
// 2. 相同请求并发时只发一次(请求去重)
if (!inflight.has(key)) {
const p = fetch(url)
.then((r) => r.json())
.then((data) => {
cache.set(key, { data, time: Date.now() });
return data;
})
.finally(() => inflight.delete(key));
inflight.set(key, p);
}
return inflight.get(key);
};
// 3. 过期但在 SWR 宽限期内:先返回旧数据,后台静默刷新
if (hit && now - hit.time < swr) {
revalidate().catch(() => {}); // 失败也不影响本次返回
return hit.data;
}
// 4. 彻底过期或无缓存:等待真实请求
return revalidate();
}✅ 边界 Case 自测:
- 并发 10 次相同请求,Network 面板应只有 1 条请求。
- SWR 返回旧数据时不能阻塞,后台刷新失败要静默吞掉,不能抛给当前调用方。
- 缓存 key 要包含查询参数和关键请求头(如语言、租户 id),否则串数据。
- 用户私有数据在退出登录时必须清空缓存,否则会泄露到下一个账号。
- 写操作(POST/PUT/DELETE)成功后要主动失效相关 key。
- 需要容量上限时换成 LRU,避免长会话内存无限增长。
🔁 变种拓展题: 用 AbortController 实现请求取消与超时;用 Service Worker 做离线缓存(Cache Storage + stale-while-revalidate 策略)。
js
function fetchWithTimeout(url, ms = 8000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms);
return fetch(url, { signal: ctrl.signal }).finally(() => clearTimeout(timer));
}⚠️ 当日踩坑实录
| 常见坑 | 解决方案 |
|---|---|
❌ CORS 配 Access-Control-Allow-Origin: *,带 Cookie 跨域直接报错 | ✅ 带凭证时禁止通配符:回显具体域名 + Access-Control-Allow-Credentials: true + 前端 withCredentials |
| ❌ 后端明明配了 CORS,浏览器仍报跨域 | ✅ 多半是 OPTIONS 预检被鉴权中间件 / 网关拦截返回了 401、404;预检要在鉴权前短路返回 204 |
❌ Access-Control-Allow-Origin 出现两次(Nginx + 后端都加) | ✅ 只在一处下发 CORS 头,重复头会被浏览器判定非法 |
❌ 动态回显 Origin 没加 Vary: Origin,CDN 把 A 域响应发给 B 域 | ✅ 动态回显必须配 Vary: Origin,并保证白名单全等匹配而非子串匹配 |
❌ 前端读不到 X-Total-Count 等自定义响应头 | ✅ 后端加 Access-Control-Expose-Headers,默认只暴露 6 个基础头 |
| ❌ 静态资源设了强缓存,发版后用户拿不到新资源 | ✅ 文件名带 contenthash(换 URL 即换缓存),index.html 固定 no-cache |
❌ 以为 no-cache 就是不缓存 | ✅ no-cache 是「每次都去协商校验」,真正不缓存是 no-store |
| ❌ 多机部署后缓存命中率骤降、ETag 频繁失效 | ✅ Nginx 默认 ETag = mtime + size,多机不一致;改为按文件内容 hash 生成 |
❌ 只用 Last-Modified,1 秒内多次修改 / 回滚旧版本导致缓存判断错误 | ✅ 启用 ETag(内容指纹),其优先级高于 Last-Modified |
| ❌ 返回 304 时带上了 body | ✅ 304 只能返回头部,带 body 会导致浏览器解析异常 |
❌ Expires 生效异常(用户改了系统时间) | ✅ 用 Cache-Control: max-age 相对时间,优先级也更高 |
| ❌ 登录态跨域丢失:请求压根没带 Cookie | ✅ Chrome 80+ 默认 SameSite=Lax,跨站 XHR 不携带;改 SameSite=None; Secure,或代理成同源 |
❌ SameSite=None 配了但 HTTP 环境下 Cookie 被直接丢弃 | ✅ None 必须搭配 Secure,即必须 HTTPS |
❌ postMessage 用 * 作为 targetOrigin / 不校验 e.origin | ✅ 发送写明确域名、接收做 origin 白名单校验(必要时校验 e.source),否则是实打实的安全漏洞 |
❌ iframe 还没加载完就 postMessage,消息丢失 | ✅ 等 onload,或让子页面先发 READY 做握手再通信 |
❌ JSONP 回调名写死,并发请求互相覆盖;用完不清理全局变量和 <script> | ✅ 回调名加时间戳 + 自增 id,成功 / 失败 / 超时三条路径统一 cleanup |
| ❌ JSONP 服务端不调用回调,Promise 永远 pending | ✅ 必须加超时兜底(onerror 捕获不到这种情况) |
| ❌ 以为跨域请求「没发出去」 | ✅ 请求已到达服务端,只是响应被浏览器拦截;排查时要看服务端日志而不是只盯前端报错 |
| ❌ 升级 HTTP/2 后仍保留域名分片、雪碧图 | ✅ 撤掉 1.1 时代 hack:域名收敛以复用单连接,雪碧图拆回独立资源以提升缓存粒度 |
| ❌ 指望 HTTP/2 在弱网一定更快 | ✅ 单连接丢包会阻塞所有流(TCP 层队头阻塞),弱网收益有限,要上 HTTP/3 / QUIC |
❌ 用 301 做临时跳转,改回来发现浏览器死活不生效 | ✅ 301 会被浏览器强缓存,临时跳转用 302 / 307 |
| ❌ 接口被浏览器缓存导致数据不更新(IE / 部分代理) | ✅ 私有数据加 Cache-Control: private, no-store,或在 URL 上加时间戳破缓存 |
| ❌ 抓包工具能看到 HTTPS 明文就以为 HTTPS 不安全 | ✅ 那是因为手动信任了抓包工具根证书;安全底座是证书信任链,移动端可做 SSL Pinning 防护 |
| ❌ 本地开发代理能登录,部署后 Cookie 失效 | ✅ devServer 的 changeOrigin / cookieDomainRewrite 只在本地生效,生产要靠 Nginx 同域或 CORS 凭证配置 |
📌 当日复盘任务
- [ ] 不看资料默画一遍浏览器缓存完整决策流程:有无缓存 → 强缓存是否过期 → 协商校验 → 304 / 200
- [ ] 用一句话说清
no-cache与no-store、max-age与s-maxage、private与public的区别 - [ ] 整理自己项目的缓存策略清单:html / 带 hash 的 js、css / 图片字体 / 接口分别配什么头,并说明理由
- [ ] 口述
ETag与Last-Modified的优先级、精度差异,以及多机部署下 ETag 不一致的成因与修复 - [ ] 打开真实项目的 Network 面板,分别用「地址栏回车 / F5 / Ctrl+F5」对比缓存命中情况,记录状态码差异
- [ ] 口述 CORS 简单请求与预检请求的判定条件与完整流程,并默写 6 个核心
Access-Control-*响应头 - [ ] 口述「跨域携带 Cookie 的四个必要条件」,并解释
SameSite三个取值的行为差异 - [ ] 列出 5 种以上跨域方案及各自适用场景,说明为什么生产环境首选 Nginx 代理 / CORS
- [ ] 手写一遍 JSONP(含唯一回调名、超时、清理),并说出它的三个固有局限
- [ ] 手写
postMessage通信的安全写法,解释不校验origin会带来什么攻击面 - [ ] 用 Node 跑通一个 demo:html 走协商缓存返回 304、静态资源命中强缓存,截图记录
- [ ] 对比 HTTP/1.1 / 2 / 3 的核心差异,并解释「HTTP/2 为什么没解决 TCP 层队头阻塞」
- [ ] 口述 HTTPS 的 TLS 握手步骤,解释为什么非对称加密只用于交换密钥
- [ ] 复述一条完整排查链路:「接口报跨域」→ 看 Network 的 OPTIONS 与响应头 → 判断是预检被拦 / 头缺失 / 凭证配置 → 定位修复
- [ ] 2 分钟内完整口述「HTTP 版本演进 + 缓存两级策略 + 跨域与 CORS」,确保逻辑连贯无卡顿