HTTP 语义与缓存与 HTTP/2 与浏览器安全边界

共 30 题
📑 题目列表 30 题
#
★★★

1. HTTP 范围请求 Range/Accept-Ranges/Content-Range 与 206、416 状态码的语义如何在断点续传与视频切片中落地?

HTTP 范围请求中 Range、Accept-Ranges、Content-Range 三个头部以及 206 Partial Content、416 Range Not Satisfiable 状态码的语义分别是什么?它们在断点续传与视频切片场景中是如何落地的?

  • Range/Accept-Ranges/Content-Range 的字段格式与语义
  • 206、416 状态码的触发条件与组合使用
  • 断点续传与视频 seek 的工程实现要点

范围请求让客户端只请求资源的字节子集。客户端用 Range: bytes=start-end(或 start- 表示从 start 到结尾、-suffix 表示末尾 N 字节)声明所需范围;服务端支持时在响应头返回 Accept-Ranges: bytes,对合法范围以 206 Partial Content 返回,并用 Content-Range: bytes start-end/total 说明本次返回区间与资源总大小。若范围非法(start 大于 end、起点超出总长等),服务端返回 416 Range Not Satisfiable,并可用 Content-Range: bytes */total 告知客户端真实总大小供其纠正偏移。

断点续传时,客户端持久化已下载偏移量,失败后用 Range: bytes=N- 从断点继续,避免重传已收数据;视频播放器在 seek 时用 Range: bytes=start-end 只拉取目标时间附近的切片,实现秒级拖动而无需全量下载。工程上服务端还应支持多段 Range: bytes=a-b,c-d 并返回 multipart/byteranges,对不带 Range 的普通请求回退为 200 全量响应,同时注意 Range 与缓存、ETag/If-Range 配合,防止内容更新后错位拼接。

回答要先讲清三个头部与两个状态码的职责边界:Range 是请求、Accept-Ranges 是能力声明、Content-Range 是响应描述,206 表达"部分成功"、416 表达"范围不可满足"。随后落到断点续传与视频 seek 两个典型场景,体现从协议语义到工程落地的完整链路。能提到 If-Range 条件请求与多段 multipart 属于加分项。

# 客户端断点续传请求
GET /movie.mp4 HTTP/1.1
Range: bytes=5242880-

# 服务端响应
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 5242880-10485759/104857600
Content-Length: 5242880
#
★★★

2. HTTP 303 See Other 与 302 Found 在浏览器 POST-Redirect-GET 模式上的差异,为什么支付回调强制使用 303?

HTTP 303 See Other 与 302 Found 在浏览器处理 POST-Redirect-GET(PRG)模式上有什么差异?为什么支付回调场景要强制使用 303?

  • 302 与 303 对 POST 重定向的方法改写规则差异
  • PRG 模式避免表单重复提交的机制
  • 支付回调中强制 303 的工程原因

302 Found 的语义是"临时重定向,保持原方法",但历史上浏览器对 POST + 302 普遍改写为 GET,不同实现行为不一致,规范最终把这种改写列为"允许但不可依赖"的宽松行为。303 See Other 的语义则是"强制以 GET 访问新地址":无论原请求是什么方法,浏览器收到 303 后一律改用 GET,且不携带原请求体,行为在所有浏览器上完全一致。

POST-Redirect-GET 模式的价值在于"刷新、后退、分享都不会重放原 POST":用户提交订单后,服务端返回 303 指向结果页,浏览器转为 GET,此时刷新页面只会重放 GET 结果请求,而不会重复创建订单。支付回调(如收银台提交、支付成功跳转)必须强制使用 303,因为涉及资金操作,若 302 的浏览器行为差异导致重定向仍以 POST 重放,可能造成重复扣款或重复下单;303 把方法转换写死在语义里,杜绝了这一风险。

核心是理解 302 的方法改写规则"依赖实现、不可保证",而 303 是"语义明确、行为统一"。回答时先对比两种状态码对 POST 的差异,再说明 PRG 模式如何借助 303 防重复提交,最后落到支付回调这种幂等性要求极高的场景,体现对状态码语义的准确掌握。

#
★★★

3. HTTP 内容协商如何用 Accept、Accept-Encoding、Accept-Language 及质量因子 q 选择表示?无匹配时为何返回 406?

HTTP 内容协商如何通过 Accept、Accept-Encoding、Accept-Language 请求头以及质量因子 q 选择资源的表示?当服务器没有任何可接受的表示时,为什么返回 406?

  • 三类 Accept 头的作用域与 q 值优先级语义
  • 服务端表示选择的判定流程
  • 406 Not Acceptable 的触发条件与意义

内容协商由客户端声明偏好、服务端选择表示。Accept 声明可接受媒体类型(如 text/html, application/json;q=0.9),Accept-Encoding 声明可接受的压缩算法(如 gzip, deflate, br),Accept-Language 声明语言偏好(如 zh-CN, zh;q=0.8, en;q=0.5)。q 值表示相对优先级,取值 0~1,缺省为 1,q=0 表示不可接受;服务端按 q 值从高到低匹配,并在同 q 值时遵循"更具体的声明优先、无声明按服务器默认"的规则。

当客户端可接受的表示集合与服务端实际提供的表示集合交集为空时,服务端返回 406 Not Acceptable,并在响应体或 Vary 相关头部中说明可用的表示,让客户端决定回退策略(如改用默认语言、改请求其他格式)。406 的意义在于显式表达"协商失败"而不是静默返回一种不满足偏好的表示,避免客户端对内容性质产生误解。现代实践中服务端常以"尽力协商 + 默认回退"降低 406 出现频率,但理解其语义仍是基础。

本题考察对协商机制的完整理解:先是三类头各自表达什么偏好、q 值如何参与排序,再是服务端选择流程,最后是 406 的语义——它是"协商失败"的显式信号而非错误。能区分"q 值决定优先级、服务器决定最终表示"的两段式过程是回答的关键。

#
★★★

4. Cache-Control 中 no-cache、no-store、private、s-maxage 的实际语义是什么?

Cache-Control 中的 no-cache、no-store、private、s-maxage 指令各自的真实语义是什么?它们分别适用于什么场景?

  • no-cache 与 no-store 的区别(缓存但校验 vs 禁止缓存)
  • private 与 public 的私有/共享缓存边界
  • s-maxage 仅对共享缓存生效的特性

no-cache 的语义是"可以存储,但每次使用前必须回源校验(revalidate)":缓存副本可用,但必须通过 If-Modified-Since/If-None-Match 等条件请求确认仍新鲜后才能返回,它并不禁止缓存。no-store 的语义是"绝不存储":浏览器、代理、CDN 都不得保留任何副本,适用于含敏感信息或强实时性要求的响应。private 表示响应只能由私有缓存(浏览器)缓存,共享缓存(代理、CDN)不得缓存,用于携带用户特定信息的响应。s-maxage 只对共享缓存生效并覆盖 max-age,用于分别控制"共享缓存有效期"与"浏览器私有缓存有效期"。

例如 Cache-Control: private, s-maxage=3600, max-age=60 表示 CDN 可缓存 1 小时而浏览器只缓存 60 秒;Cache-Control: no-store 用于支付结果、账号信息等不可落盘的响应;no-cache 常用于动态但可复用的页面,缓存命中后仍需回源确认。组合使用时要注意:no-cache 可与 s-maxage 等共存,而 no-store 优先于一切缓存指令;不加 private/public 时默认允许所有缓存存储。

这四个指令最容易混淆的是 no-cache 与 no-store,前者"存了但每次要用必须验证",后者"根本不存"。private 与 s-maxage 则围绕"私有缓存 vs 共享缓存"的双层缓存模型展开,是 CDN 与浏览器缓存策略协同的关键。回答按"存储限制"与"新鲜度控制"两个维度组织,就能把语义讲全。

#
★★★

5. Vary: Accept-Encoding 与 Vary: Accept-Language 对 CDN 缓存命中率的影响,缓存键设计时应避免哪些过度膨胀?

Vary: Accept-Encoding 与 Vary: Accept-Language 对 CDN 缓存命中率有什么影响?设计缓存键时应如何避免过度膨胀?

  • Vary 头如何把请求头纳入缓存键
  • 缓存键膨胀对命中率与存储的负面影响
  • 归一化与最小化 Vary 的工程策略

Vary 头告知缓存:响应内容会因指定请求头的取值不同而不同,因此缓存键必须拼接这些请求头的值。Vary: Accept-Encoding 会把 gzip、br、identity 等压缩变体各自独立缓存;Vary: Accept-Language 会把不同语言版本拆分为不同缓存条目。这保证了缓存正确性,但每个 Vary 字段都会把一个缓存条目按取值基数拆分成多个子条目,访问被稀释、命中率下降;若 Vary 了高基数字段(如完整 User-Agent 字符串、Accept-Encoding 的任意组合),缓存键可能爆炸,存储被大量"一次性条目"占满,命中率骤降。

设计缓存键时应最小化 Vary 字段:只对真正改变表示语义的头部做 Vary;对取值做归一化,例如压缩统一收敛到 gzip, br 两个固定值、语言按"区域回退主语言"折叠;高基数字段应移出缓存键,改用 URL 参数、Cookie 或规范化的请求属性区分。此外可配合 CDN 的"忽略部分请求头"配置与规范化中间件,在保证正确性的前提下把变体数量压到可控范围。

本题考察"缓存键 = URL + Vary 引入的请求头值"这一核心模型,以及正确性与命中率之间的权衡。先解释 Vary 如何扩大缓存键,再分析膨胀的后果,最后给出归一化、最小化的工程策略,体现从原理到实践的系统思考。

#
★★★

6. Cookie 的 Path、Domain、SameSite、Secure、HttpOnly、__Host- 前缀组合如何防御 CSRF/XSS/会话固定攻击?

Cookie 的 Path、Domain、SameSite、Secure、HttpOnly 属性与 __Host- 前缀如何组合起来防御 CSRF、XSS 与会话固定攻击?

  • 各 Cookie 属性的作用域与安全语义
  • SameSite 对 CSRF 的防御机制
  • HttpOnly 对 XSS 窃取的阻断与 __Host- 对会话固定的防御

HttpOnly 阻止脚本通过 document.cookie 读取 Cookie,阻断 XSS 窃取会话的路径;Secure 使 Cookie 仅在 HTTPS 连接上传输,防止明文链路窃听;SameSite=Lax/Strict 控制跨站请求是否携带 Cookie:Lax 允许 top-level GET 导航携带、阻止跨站 POST 与表单提交携带,Strict 完全禁止跨站携带,是对抗 CSRF 的关键;Domain 限定 Cookie 生效的域、Path 限定发送路径,设置过宽(如 Domain=父域)会扩大攻击面。__Host- 前缀强制 Cookie 必须同时满足 Secure、Path=/、无 Domain 三个条件,从源头防止前缀注入与域名污染。

会话 Cookie 的推荐组合是 Set-Cookie: __Host-session=...; Secure; HttpOnly; Path=/; SameSite=Lax:HttpOnly 防 XSS 窃取、SameSite=Lax 防跨站请求伪造、Secure 防窃听、__Host- 防会话固定与域名污染,四条攻击路径同时封死。需要说明的是各属性防的攻击面不同,单靠任何一项都不完整:SameSite 不防同站 XSS、HttpOnly 不防 CSRF、Secure 只保证传输层,因此必须组合使用形成纵深防御。

回答的关键是"属性与攻击类型一一对应":CSRF 由 SameSite 防、XSS 窃取由 HttpOnly 防、会话固定/域名污染由 __Host- 与 Domain 约束防、窃听由 Secure 防。最后给出组合方案说明纵深防御,是这类安全题的完整答法。

HTTP/1.1 200 OK
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; Path=/; SameSite=Lax
Set-Cookie: theme=dark; Path=/; Max-Age=2592000
#
★★★

7. If-Modified-Since 只精确到秒且属弱校验,经过多级代理时为何精度与一致性会下降?与 ETag 相比有何局限?

If-Modified-Since 为什么只精确到秒且属于弱校验?经过多级代理时它的精度与一致性为什么会下降?与 ETag 相比它有什么局限?

  • Last-Modified/If-Modified-Since 的秒级精度与弱校验特性
  • 多级代理场景下时间戳不一致的成因
  • ETag 强校验在一致性与精确性上的优势

HTTP 日期格式只能精确到秒,因此基于时间的验证器粒度是秒级;更重要的是它属于弱校验(weak validator):Last-Modified 只反映"最后一次修改时间",无法区分同一秒内的多次修改,也无法表达"内容虽改但时间戳未变"的情况,代理还可能重写或修正时间戳。经过多级代理时,各跳服务器时钟不一定同步(NTP 偏差),且代理可能各自持有不同的缓存副本与时间记录,客户端用某级代理的 Last-Modified 去另一级代理校验时就会出现不一致,条件请求的准确性进一步下降。

ETag 是基于实体内容生成的验证器,强 ETag 保证"内容字节相同则 ETag 相同、内容不同则 ETag 必然不同",不受时钟同步影响,在多级代理、分布式服务端之间保持一致,能精确反映字节级变化,因此 HTTP 规范建议优先使用 ETag 做强校验。实践中常两者并存:先发 If-None-Match + If-Modified-Since,服务端优先处理 ETag;同时缓存条目同时记录两者,配合 304 响应中的 Last-Modified 更新兜底。

本题的答法是"弱校验的根源在时间戳的粒度与语义"。先讲秒级精度与弱校验的定义,再分析多级代理下时钟漂移与副本不一致如何放大问题,最后对比 ETag 的内容指纹特性,说明强校验为何是首选、时间校验只能兜底。

#
★★★

8. Vary 头部如何依据内容协商的请求头划分缓存变体?过多 Vary 字段为何会拉低缓存命中率?

Vary 头部如何依据内容协商的请求头划分缓存变体?过多的 Vary 字段为什么会拉低缓存命中率?

  • Vary 把响应变体与请求头值绑定为缓存键的机制
  • 变体数量膨胀的数学效应与命中率稀释
  • 控制 Vary 数量的工程手段

内容协商中同一 URL 可因 Accept、Accept-Encoding、Accept-Language 等请求头产生不同表示,Vary 告诉缓存"响应取决于哪些请求头",缓存因此把缓存键扩展为"URL + 所列请求头的值",每种取值组合对应一个独立变体条目。例如 Vary: Accept-Encoding, Accept-Language 时,gzip+zh-CNbr+en 等组合各自成键,缓存命中时须同时匹配 URL 与全部请求头值才算命中。

每个 Vary 字段都把缓存条目按该头的取值基数拆分,多个字段的效果是取值的笛卡尔积:N 个字段、每个字段 M 个取值就产生 M 的 N 次方量级的潜在变体。请求分布不均匀时大量变体只有一次访问,缓存被"缓存碎片"占满,命中率与内存利用率同步下降。工程上应只对真实影响表示的头做 Vary,尽量做值归一化(压缩类型收敛、语言回退折叠),并把高基数维度(如 User-Agent 全文、随机参数)移出 Vary,改用规范化后的键或 URL 区分。

回答先讲清"Vary 把请求头值并入缓存键"这一机制,再解释变体数量按笛卡尔积膨胀、访问被稀释导致命中率下降的因果链,最后给出归一化与最小化 Vary 的实践建议,形成"机制—影响—对策"的完整结构。

#
★★★

9. 代理返回陈旧内容时,如何检查 Age、Vary、Date、缓存键与 stale-while-revalidate?

当代理返回陈旧内容时,如何通过 Age、Vary、Date、缓存键与 stale-while-revalidate 进行排查与决策?

  • Age 与 Date 计算新鲜度、识别陈旧响应的方式
  • Vary 与缓存键对"错误命中"的定位
  • stale-while-revalidate 在源站故障时的行为与代价

Age 头表示响应在缓存系统中已被缓存的时间(秒),Date 表示源站生成响应的时间,两者结合可推算当前"已存在时长"并与 max-age/s-maxage 比较判断是否过期;若返回内容的 Age 远超缓存新鲜度窗口,说明代理直接命中陈旧副本。检查 Vary 与缓存键则定位"错误命中":当同一 URL 因 Accept-Encoding、Accept-Language 等头被拆分为多个变体时,客户端请求头若与缓存变体不匹配,可能命中错误语言或错误压缩版本,表现为"内容陈旧"或"内容不对"。

stale-while-revalidate 允许缓存代理在副本过期后先返回陈旧内容给用户、同时后台异步回源刷新,前端可用性不因源站慢而中断;排查时应结合 Age、响应头中的 Cache-Control(是否带 stale-while-revalidate)、回源日志确认是"主动发放陈旧内容"还是"无意命中过期缓存"。若源站持续故障,stale-if-error 还可让代理在源站报错时发放陈旧内容兜底。定位顺序是:先看 Date/Age 判断新鲜度,再看 Vary/缓存键判断命中是否正确,最后结合源站可用性判断该走回源、刷新还是兜底策略。

本题是"缓存排障"综合题,核心是区分"合法陈旧(revalidate 机制主动发放)"与"异常陈旧(过期未刷新)"。回答按"计算新鲜度(Age/Date)→ 核对命中键(Vary/缓存键)→ 决策动作(回源/刷新/兜底)"三步展开,最后落到 stale-while-revalidate 与 stale-if-error 的适用边界。

#
★★★

10. HEAD 与 GET 在性能测试中的等价性,搜索引擎优化为何仍存在差异?304 Not Modified 与 200 OK 的带宽差异如何度量?

HEAD 与 GET 在性能测试中是否等价?为什么搜索引擎等场景中两者仍有差异?304 Not Modified 与 200 OK 的带宽差异应如何度量?

  • HEAD 与 GET 的语义关系(无响应体 vs 带响应体)
  • 服务端实现差异导致的 HEAD/GET 行为不一致
  • 304 与 200 的字节级带宽差异度量

HEAD 的语义是"与 GET 相同但响应不包含消息体",只返回头部;性能测试中常用 HEAD 探测 URL 可用性与响应头,但两者并不严格等价:服务端可能对 HEAD 走轻量处理路径(不渲染模板、不查询大字段),也可能因实现缺陷返回与 GET 不一致的头部(如 Content-Length 缺省、Vary 缺失),因此 HEAD 的时延与头部结果不能完全代表 GET。搜索引擎爬虫使用 HEAD 探测链接可快速评估资源存在性与类型,但要获得正文与完整语义(如规范化 URL、正文指纹)仍需 GET,这就是两者在 SEO 场景下仍存在差异的原因。

度量 304 与 200 的带宽差异,可对比两种情况下的传输字节:200 OK 携带完整实体(Content-Length 为资源大小),304 Not Modified 只携带状态行与必要头部(通常几十到几百字节),不携带实体。度量方法是在网关/代理/浏览器 DevTools 中分别记录 200 与 304 的响应字节数(含 HTTP 头与体的实际线上字节),计算"200 全量下载字节数 − 304 条件响应字节数",再乘以命中次数即可估算节省的带宽;理想压缩场景下 304 可把重复下载的流量压缩到 1% 以下。

本题考察"语义等价 ≠ 实现等价"的工程判断:HEAD 无响应体是其定义,但性能指标与头部结果会受服务端实现影响,不能直接替代 GET 压测。304 与 200 的带宽度量则落在字节层面:用实际传输字节而非 Content-Length 名义值对比,才能反映真实节省。

#
★★★

11. HTTP multipart/form-data 与 application/x-www-form-urlencoded 在大文件与二进制上传场景下的工程取舍?

HTTP 的 multipart/form-data 与 application/x-www-form-urlencoded 两种编码类型在大文件与二进制上传场景下各有什么特点?工程上应如何取舍?

  • 两种编码类型的格式与字符处理差异
  • 二进制与大文件场景下的效率与兼容性
  • 上传方案选型的工程判断

application/x-www-form-urlencoded 把所有键值对按"键=值&键=值"编码并用百分号转义特殊字符,格式紧凑、解析简单,适合少量文本字段(如表单登录),但对二进制数据要先做 URL 编码(体积膨胀接近 2 倍以上:非 ASCII/非保留字节均编码为 %XX,1 字节变 3 字节)且难以表达文件边界。multipart/form-data 以 boundary 分隔多个 part,每个 part 可携带独立的 Content-Type 与内容,直接承载原始字节流,无需转义,是文件上传的标准选择;浏览器 FormData 与多数服务端框架原生支持,且一个请求可同时携带文件与普通字段。

工程取舍上:小文本表单用 urlencoded 即可;大文件、二进制内容、多文件混合表单必须用 multipart/form-data;超过百 MB 级文件通常还要叠加分片上传(前端切片 + 服务端合并)、断点续传与秒传(内容哈希比对)能力,此时 multipart 仍是单分片的传输格式。性能上 multipart 的开销主要在 boundary 解析与内存缓冲,流式处理(不整体读入内存)可降低大文件上传的内存峰值。

回答的核心是"编码格式决定字节形态":urlencoded 面向文本键值、需转义,multipart 面向多 part 原始字节、无需转义。先对比两种格式,再给出大文件/二进制的选型结论,最后补充分片、断点续传等工程能力,体现方案完整性。

#
★★★

12. HTTP/2 的流、帧、连接级流控如何配合,多路复用为何仍可能受 TCP 队头阻塞?

HTTP/2 的流(stream)、帧(frame)与连接级流控如何配合工作?多路复用为什么仍可能受到 TCP 队头阻塞的影响?

  • HTTP/2 流、帧、连接的分层模型
  • 流级与连接级流量控制的配合机制
  • TCP 队头阻塞在多路复用下的残留原因

HTTP/2 在一条 TCP 连接上复用多条逻辑流:流(stream)是请求/响应的逻辑通道,由流 ID 标识;帧(frame)是传输的最小单位,HEADERS、DATA、SETTINGS、WINDOW_UPDATE 等帧承载头部与数据,并通过流 ID 归属到具体流;多个流的帧交错(interleaving)在同一连接上传输。流量控制分两层:连接级窗口限制整条连接在途字节数,流级窗口限制单流在途字节数,接收方通过 WINDOW_UPDATE 帧动态调整窗口,防止慢速消费者被快速生产者淹没。

多路复用消除了 HTTP/1.1 的应用层队头阻塞(一个响应未完成不会阻塞其他响应),但底层仍是单条 TCP 连接:TCP 保证字节有序传输,一个 TCP 段丢失会触发重传并阻塞后续所有已到达但乱序的数据,直到丢失段补上,这期间所有流的帧都无法交给应用层,即 TCP 队头阻塞。它发生在传输层,HTTP/2 无法消除,只能靠 QUIC/HTTP/3 的独立流解决。这也是高丢包、高 RTT 链路上 HTTP/2 性能反而不如 HTTP/1.1 多连接的原因。

回答分两层:先讲清"连接承载流、流承载帧、流控分两级"的分层模型,再点明多路复用消除的是应用层队头阻塞、而 TCP 层丢包重传仍会造成跨流阻塞,最后引出 HTTP/3/QUIC 的解决思路,形成完整因果链。

#
★★★

13. HTTP/2 server push 为何在 Chrome 中默认关闭,103 Early Hints 与 preload link 关系如何?

HTTP/2 server push 为什么在 Chrome 中被默认关闭?103 Early Hints 与 preload link 之间是什么关系?

  • server push 的实现复杂度与缓存浪费问题
  • 103 Early Hints 提前下发头部的工作方式
  • 与 preload link 的互补关系与工程选型

HTTP/2 server push 允许服务端在响应前主动推送资源,但存在多个工程问题:推送无法感知浏览器缓存状态,已缓存资源会被重复推送浪费带宽;推送资源的优先级难以估计,可能抢占关键资源带宽;与 CDN 缓存、代理的交互复杂,Nginx 等反代对 push 支持有限。因此 Chrome 在 2019 年后默认关闭了 push 支持(HTTP/2 规范也随之弱化其使用建议),改为推荐 103 Early Hints 与 preload。

103 Early Hints 是服务端在处理请求期间提前发送的 1xx 信息性响应,可携带 Link: rel=preload 头部,让浏览器在最终响应(200)到达前就并行预加载关键子资源,降低首屏等待;preload link 则可由响应头或 HTML 标记声明"该资源需要提前加载"。关系上:103 解决的是"提前告知时机"(在最终响应之前),preload 解决的是"加载哪些资源",两者常组合使用——103 携带 preload 链接头实现推送效果而不越权猜测缓存状态,浏览器仍按自身缓存与优先级决策,因此更稳健。

回答要先讲清 server push 被弃用的根因(缓存浪费、优先级难控、代理兼容差),再解释 103 的机制(1xx 先行头部)与 preload 的机制(声明资源),最后说明两者的组合关系与相对 push 的优势,体现协议演进脉络。

#
★★★

14. TLS 1.3 与 ALPN 协商在 HTTP/2、HTTP/3 部署中的 h2、h2c、h3 标识差异,未加密 HTTP/2 适用场景是什么?

TLS 1.3 与 ALPN 协商在 HTTP/2、HTTP/3 部署中的 h2、h2c、h3 标识有什么区别?未加密 HTTP/2(h2c)的适用场景是什么?

  • ALPN 协议标识 h2、h2c、h3 的含义
  • TLS 1.3 与 ALPN 的握手配合
  • h2c 的部署限制与适用场景

ALPN(Application-Layer Protocol Negotiation)是 TLS 扩展,在握手期间让客户端与服务端协商应用层协议:客户端在 ClientHello 中携带支持的协议标识列表,服务端在 ServerHello 中选定一个返回。标识 "h2" 表示 HTTP/2 over TLS,"h2c" 表示 HTTP/2 over cleartext TCP(明文),"h3" 表示 HTTP/3(基于 QUIC/UDP)。实际部署中,HTTPS 站点通过 ALPN 协商 h2/h3;TLS 1.3 的 1-RTT 握手与 ALPN 配合可在单次往返内完成"加密 + 协议协商",若 ALPN 协商出 h2 但服务端不支持,或连接为明文,浏览器/客户端就无法使用 HTTP/2。

h2c 的适用场景非常有限:它没有加密、没有 ALPN 参与(明文无法协商,只能通过 Upgrade: h2c 头或 prior knowledge 建立),主要出现在内网代理、调试工具、负载均衡器之间链路已由其他手段加密(如 IPsec、VPN)的内部环境,或作为测试桩。生产公网环境应禁止 h2c,一律使用 h2/h3 over TLS;浏览器从未支持过 h2c,它只存在于非浏览器客户端与服务端实现中。

回答先解释 ALPN 机制与三个标识的映射关系,再强调"h2 依赖 TLS+ALPN、h3 依赖 QUIC 的 ALPN、h2c 是明文特例",最后给出 h2c 仅适合受信内网/调试的结论,体现安全边界意识。

#
★★

15. HTTP/2 帧级流量控制(window update)的初始窗口、MAX_FRAME_SIZE 与单向流控策略对响应延迟的影响?

HTTP/2 帧级流量控制中初始窗口、MAX_FRAME_SIZE 与单向流控策略如何影响响应延迟?

  • 流控窗口的协商机制与默认初始值
  • MAX_FRAME_SIZE 对帧粒度与吞吐的影响
  • 单向/双向流控策略与延迟的权衡

HTTP/2 流量控制由 SETTINGS 帧协商:SETTINGS_INITIAL_WINDOW_SIZE 定义流级初始发送窗口(默认 65535 字节),SETTINGS_MAX_FRAME_SIZE 定义单帧最大负载(默认 16384、最大 16777215);接收方通过 WINDOW_UPDATE 帧追加信用,发送方只有在窗口内才能发送 DATA。窗口过小会频繁触发"发满→等 WINDOW_UPDATE→再发"的停顿,增加往返次数,直接抬高响应延迟;窗口大则允许发送方一次性灌注更多数据,减少停顿但可能让慢消费者缓冲膨胀,因此高带宽延迟积(BDP)链路上应调大初始窗口。

MAX_FRAME_SIZE 影响帧粒度:帧越小,交错调度越细、优先级越容易生效,但帧头开销占比上升;帧越大,吞吐越高、但单帧抢占时间变长。单向流控策略指只对单向数据流设窗口(如仅限制服务端→客户端方向),适用于带宽不对称场景,避免反向信令占用过多窗口信用;超时等待窗口信用会表现为"发送停滞",需结合 BDP 估算合理窗口。实际调优中,Nginx、Envoy 等常把初始窗口调至 64KB~1MB 级别以匹配内网或公网高 BDP。

本题考察流控参数与延迟的定量关系:窗口决定"一次能发多少",帧大小决定"调度粒度",两者共同影响停顿频率与吞吐。回答按"窗口协商→帧大小→单向策略"三层展开,并落到 BDP 匹配与调优建议,体现对协议细节的工程理解。

#
★★

16. HTTP/2 升级 HTTPS 与 HTTP/2 明文(h2c)的部署场景差异,Spring/Netty 服务器如何协商?

HTTP/2 over TLS(h2)与 HTTP/2 明文(h2c)的部署场景有什么差异?Spring Boot、Netty 等服务器是如何协商 HTTP/2 的?

  • h2 与 h2c 的部署场景与安全边界
  • ALPN 与 Upgrade 头两种协商路径
  • Spring/Netty 对 HTTP/2 的支持方式

h2 是生产环境的唯一标准形态:浏览器仅支持 ALPN 协商出的 h2(必须 TLS),TLS 1.2/1.3 + ALPN 在握手内完成协议选择,适用于公网、CDN、需要加密与安全性的所有场景。h2c 不加密,只能通过 HTTP/1.1 的 Upgrade: h2c 头升级建立,或由客户端"prior knowledge"直接以明文发送 HTTP/2 帧;它无法被浏览器使用,仅适合受信内网、代理链内部、本地调试,且需确认中间链路已有其他加密手段兜底。

Spring Boot(嵌入式 Tomcat/Netty)在启用 HTTPS 并配置 h2 后,由 Tomcat 或 Netty 的 TLS 实现自动完成 ALPN 协商;未启用 TLS 时,Spring Boot 默认不支持 h2c 的 Upgrade(需显式配置 HTTP/2 cleartext 支持,如 Undertow/Tomcat 的 Upgrade 配置)。Netty 则通过 Http2MultiplexHandler、Http2FrameCodec 直接操作 HTTP/2 帧层,ALPN 由 JSSE 的 AlpnHandler(或 JDK 9+ 内置 ALPN)完成。选型要点:公网一律 h2+TLS;内网若必须 h2c,需确保链路信任边界,并测试代理对 Upgrade/prior knowledge 的支持。

回答先对比两种形态的部署边界(是否加密、浏览器是否可用),再分别讲清 ALPN 与 Upgrade 两条协商路径,最后落到 Spring/Netty 的具体实现机制,体现从规范到框架的完整映射。

#
★★

17. 浏览器缓存分区(cache partition)启用后跨站缓存命中率显著下降,灰度回滚与 A/B 评估方案如何设计?

浏览器缓存分区(cache partition)启用后跨站缓存命中率为什么显著下降?针对这一变化,灰度回滚与 A/B 评估方案应如何设计?

  • 缓存分区的机制与跨站共享缓存失效的原因
  • 命中率下降的业务影响面分析
  • 灰度发布、回滚与 A/B 评估的方法

缓存分区(cache partitioning)按"顶层站点 + 请求方站点"双重维度隔离缓存:第三方的同一个 URL 在来自不同站点时被当作不同缓存条目,第三方 CDN 资源的跨站共享缓存被拆散,重复下载增加,表现为缓存命中率显著下降。这是浏览器为防御"跨站缓存时序侧信道"(如探测用户是否访问过某站点)所做的安全取舍,属不可关闭的强制行为,影响面集中在第三方资源(统计脚本、广告、共用静态库、Web 字体等)。

灰度与 A/B 评估的核心是"先量化影响,再决定动作":用 RUM(Real User Monitoring)或服务端访问日志对比灰度前后的缓存命中率、首字节时间、总流量与核心页面性能指标,按站点维度拆分看哪些业务受影响最大;评估维度包括命中率下降幅度、页面 LCP 退化、带宽成本上升。回滚并非指关闭浏览器特性(无法关闭),而是回退业务侧应对:例如撤回"依赖跨站共享缓存"的假设,改用同站聚合、资源内联、Service Worker 缓存或自托管同源资源。发布节奏上先小流量灰度观察关键指标,确认影响可控后再全量,同时保留按站点/按资源类型开关的配置化能力以便快速回退。

回答要先讲清缓存分区的机制与设计动机(安全隔离),再明确"命中率下降是必然结果、回滚对象是业务策略而非浏览器行为",最后给出以 RUM 数据驱动的灰度评估框架与回退手段,体现对浏览器安全演进与工程应对的双重理解。

#
★★

18. 安全方法与幂等方法如何区分,PUT、PATCH、POST 在重试策略上应如何处理?

HTTP 安全方法(safe)与幂等方法(idempotent)如何区分?PUT、PATCH、POST 在重试策略上应如何处理?

  • 安全性与幂等性的定义与判定
  • PUT/PATCH/POST 的幂等属性差异
  • 基于幂等性设计重试与去重策略

安全方法指不会改变服务器状态的方法(GET、HEAD、OPTIONS、TRACE),可被缓存与预取;幂等方法指"执行一次与执行多次效果相同"的方法(GET、PUT、DELETE、HEAD、OPTIONS、TRACE),即重放不产生额外副作用。两者的区分维度不同:安全关心"是否改变状态",幂等关心"重复执行是否一致"。GET 既安全又幂等;PUT 不一定是安全的(会覆盖资源)但幂等;DELETE 幂等但不安全。

POST 既不安全也不幂等,每次执行都可能创建新资源或触发新副作用,因此网络超时后的自动重试是危险的(可能重复下单、重复扣款),必须依赖业务幂等键(Idempotency-Key)去重,或由用户显式确认后重试。PUT 语义是"整体替换",重复执行结果一致,可安全重试;PATCH 语义是"部分更新",其幂等性取决于补丁内容(按绝对字段赋值则幂等,按增量计算如 +=1 则非幂等),规范建议 PATCH 实现为幂等或返回 422 提示;不确定时应采用"条件请求(If-Match)+ 版本号"或幂等键保证重试安全。

回答先给出安全与幂等的定义并对比判定维度,再逐个分析 PUT、PATCH、POST 的幂等属性,最后落到重试策略:幂等方法可直接自动重试,非幂等 POST 必须去重或人工确认,PATCH 需结合实现确认幂等性,体现"HTTP 语义指导工程容错"的思路。

#
★★

19. HPACK 动态表如何压缩重复首部,敏感字段为何应使用 never-indexed 表示?

HPACK 动态表如何压缩重复出现的 HTTP 首部?为什么 Authorization 等敏感字段应使用 never-indexed 表示?

  • HPACK 静态表与动态表的协作机制
  • 首部索引化的过程与增量编码
  • never-indexed 对敏感字段的保护原理

HPACK 用"索引表 + 首部名索引 + 增量编码"压缩重复首部:静态表预置常见首部(如 :method: GET、:path、content-type 等)的固定索引;动态表由双方维护的滑动窗口(受 SETTINGS_HEADER_TABLE_SIZE 限制,默认 4096 字节)保存连接期间出现过的自定义首部名值对,新首部可引用已有索引,或只编码新值并插入表尾,表满时按 LRU 淘汰。因此同一连接内重复出现的首部(如 User-Agent、cookie 前缀、自定义元数据)第二次起只需 1~4 字节的索引引用,压缩率大幅提升。

never-indexed 表示告诉 HPACK 编码器"不要把这个首部加入动态表、也不要用已索引形式引用",用于保护敏感字段:若 Authorization 等被索引进动态表,任何能观察到连接内后续帧或共享 HPACK 状态的一方都可能通过索引号推断出字段值,且动态表内容还可能出现在连接的其他上下文(如调试转储)中。使用 never-indexed 后该字段每次都以字面量形式传输(值仍可被 TLS 加密,但不参与索引),安全性更高、压缩率略降。实践中还应避免把完整 cookie、token 写入日志或非必要首部。

回答先讲清 HPACK 静态表/动态表与索引引用的压缩机制,再解释"索引意味着值在连接内可复用、可观察"这一安全属性,说明敏感字段为何要绕过索引以字面量传输,体现压缩与安全的权衡意识。

#
★★

20. 遇到 421、425、429、503 时,客户端应分别依据哪些语义决定重连、延迟或重试?

客户端遇到 421、425、429、503 状态码时,应分别依据哪些语义决定重连、延迟还是重试?

  • 各状态码的语义与触发场景
  • 421/425 的重连语义、429/503 的重试语义
  • 重试策略(指数退避、Retry-After)的落地

421 Misdirected Request 表示请求被发送到了无法处理该主机/URI 的连接(多租户服务端或连接复用错配),客户端应"换连接重发",即新建或复用指向正确主机/虚拟主机的连接,而不是在旧连接上重试。425 Too Early 表示服务端担心请求携带的早期数据(Early Data/0-RTT 重放)可能不安全,客户端应关闭连接并重新发起完整握手后重试,通常配合 Retry-After 使用。429 Too Many Requests 表示触发限流,客户端应遵循响应中的 Retry-After 或按指数退避延迟重试,避免加剧限流。

503 Service Unavailable 表示服务端临时过载或维护中,客户端可按 Retry-After 延迟重试,未提供 Retry-After 时用指数退避(如 1s、2s、4s 加倍并加抖动);应区分"可重试"与"不可重试":429/503 属临时性可重试,421/425 需要先纠正连接/握手状态再重试,而 4xx 业务错误(400、403、404)不应盲目重试。工程上统一由重试框架(如 resilience4j、gRPC retry policy)按状态码分类配置:连接级问题重连、限流退避、幂等请求重放。

本题考察状态码语义到重试动作的映射:421 是连接归属错误(换连接)、425 是 0-RTT 数据风险(重握手)、429 是限流(退避)、503 是临时不可用(延迟重试)。回答按状态码逐个给出客户端动作,并总结"重试对象与动作"的分类框架,体现工程化容错设计。

#
★★

21. HPACK 动态表压缩爆破攻击(HPACK bomb)与 Nginx、Envoy 的最大表大小限制策略?

什么是 HPACK 动态表压缩爆破攻击(HPACK bomb)?Nginx、Envoy 等代理如何通过限制最大表大小进行防御?

  • HPACK 表膨胀攻击的放大原理
  • SETTINGS_HEADER_TABLE_SIZE 的协商与限制
  • Nginx/Envoy 的头部大小与表大小防护配置

HPACK 动态表允许压缩比极高的编码:发送方可通过索引引用和增量编码把"很小的在线字节"展开为"很大的解码结果",例如利用索引 62+(动态表引用)与字符串字面量的组合构造炸弹,让接收方解压后占用远超线网字节的内存(放大倍数可达数百倍)。攻击者向代理或服务器发送精心构造的 HPACK 流,使接收方内存耗尽,即 HPACK bomb;2016 年的 HTTP/2 实现漏洞(如 nghttp2 的 CVE-2016-1544)多与此类表膨胀和头部块解码有关。

防御的核心是限制"解码端的资源上限":连接级通过 SETTINGS_HEADER_TABLE_SIZE(默认 4096 字节)协商双方动态表上限,禁止超过;实现层对单个头部块、单个首部值长度、首部总条数与总字节数设硬上限,超过即报错并 RST_STREAM 或断连。Nginx 提供 http2_max_field_size、http2_max_header_size 等指令限制单字段与整块头部大小;Envoy 通过 http_connection_manager 的 max_request_headers_kb、max_request_headers_count 及 header map 限制控制;此外还有每连接解码内存预算与整体内存配额兜底,防止单连接耗尽进程内存。

回答先讲清 HPACK bomb 的放大机制(小字节→大内存的解压扩张),再说明两层防御:协议层 SETTINGS 表大小协商与实现层头部大小/条数硬限制,最后落到 Nginx、Envoy 的具体配置项,体现"了解攻击原理、掌握防御配置"的完整链路。

#
★★

22. 强 ETag、弱 ETag、Last-Modified 如何用于条件请求,If-Match 与 If-None-Match 分别防止什么问题?

强 ETag、弱 ETag 与 Last-Modified 如何用于条件请求?If-Match 与 If-None-Match 分别用于防止什么问题?

  • 强/弱 ETag 的表示与判定语义
  • 条件请求头与验证器的配对使用
  • If-Match 的并发写防护与 If-None-Match 的缓存校验

强 ETag 以 "abc123" 形式表示,要求字节级等价(响应体逐字节相同才视为同一实体);弱 ETag 以 W/"abc123" 前缀表示,只要求语义等价(渲染结果可不同但语义相同),弱 ETag 不能用于 Range 请求的 If-Range 校验。Last-Modified 是时间型弱验证器,常与 ETag 并存。条件请求头与验证器配对:If-None-Match 携带 ETag 列表(或 *),匹配时返回 304(GET/HEAD)或 412(非 GET),用于缓存新鲜度校验,防止重复传输;If-Match 携带期望的 ETag,不匹配时返回 412 Precondition Failed,用于"乐观并发控制"。

If-Match 防止"写覆盖":客户端先 GET 拿到 ETag,更新资源时带 If-Match: 该 ETag,若期间资源被他人修改(ETag 变化),服务端拒绝写入(412),避免丢失更新(lost update);也可用 If-Match: * 表示"仅当资源存在时才写"。If-None-Match 则防止"读重复"与"写重复":缓存校验场景返回 304 省带宽,配合 POST 可做"仅当不存在时创建"(防止重复提交)。Range 请求用 If-Range 结合强 ETag 或时间戳,确保断点续传数据与当前版本一致,弱 ETag 与 Last-Modified 不满足 If-Range 的强校验要求。

回答按"验证器(强/弱 ETag、Last-Modified)→ 条件请求头(If-Match/If-None-Match/If-Range)→ 防止的问题(缓存浪费、丢失更新、重复创建)"三层展开,注意强/弱 ETag 的适用边界,尤其 If-Range 必须用强验证器,体现对条件请求体系的完整理解。

#
★★

23. Transfer-Encoding: chunked 与 Content-Length 为何互斥?两者同时出现时请求走私(request smuggling)如何发生?

Transfer-Encoding: chunked 与 Content-Length 为什么互斥?两者同时出现时请求走私(request smuggling)是如何发生的?

  • 消息体长度的两种确定方式与互斥规则
  • 前后端对长度解析不一致导致的走私
  • 请求走私的防护手段

消息体长度只能由 Content-Length(定长)或 Transfer-Encoding: chunked(分块)二者之一确定,同时出现时规范要求忽略 Content-Length、以 chunked 为准。chunked 把消息体分成多个块,每块由"十六进制长度 + CRLF + 块数据"构成,以 0 长度块结尾。两者互斥的意义在于消除长度歧义:若代理按 Content-Length、后端按 chunked(或相反)解析,同一请求的边界划分不一致,攻击者可构造"前段被代理视为完整请求、后段被后端视为新请求"的畸形请求,实现请求走私。

典型走私场景:攻击者发送 Content-Length: 6Transfer-Encoding: chunked 且 body 中伪造另一条请求,前端代理按 CL=6 截断、把剩余字节当作下一请求;后端按 chunked 解析则把伪造请求当作本次请求的延续处理,从而绕过代理的鉴权、缓存污染或投毒他人请求。防护手段:规范明确同时出现时按 chunked 处理,若无法解析 chunked 则返回 400;代理与后端统一使用同一长度判定逻辑(如统一关闭 TE 转发、剥离 Transfer-Encoding 头、规范化 Content-Length),并对畸形 TE 头直接拒绝(RFC 7230 要求收到无法理解的 TE 时返回 400);现代网关(如 Envoy、Nginx)默认对 CL+TE 冲突请求拒绝或改写。

本题的核心是"长度歧义 = 边界分歧 = 走私窗口"。回答先讲清两种长度机制的互斥规则,再构造"代理按 CL 截断、后端按 chunked 解析"的走私链路,最后给出规范化与拒绝畸形请求的防护策略,体现对 HTTP 解析安全的理解。

# 攻击者构造的冲突请求(CL 与 TE 并存)
POST /pay HTTP/1.1
Host: bank.example.com
Content-Length: 6
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: bank.example.com
#
★★

24. Expect: 100-continue 让客户端在发送请求体前先等服务器确认,它在大文件上传中如何节省带宽?服务器不响应 100 时客户端如何处理?

Expect: 100-continue 让客户端在发送请求体前先等待服务器确认,它在大文件上传中如何节省带宽?服务器不响应 100 时客户端应如何处理?

  • 100-continue 的两阶段握手流程与目的
  • 大文件上传中避免无效传输的带宽节省原理
  • 服务器不响应 100 时的超时与回退策略

Expect: 100-continue 是客户端对大型请求体的"预检"机制:客户端先发送含该头的请求头(不含体),服务器若愿意接收则回 100 Continue,客户端才开始发送消息体;若服务器因鉴权失败、体超限等原因拒绝,则直接回 4xx,客户端连请求体都不用发送。在大文件上传场景中,这避免了"鉴权失败/参数非法却仍把整个大文件传完"的带宽与时间浪费,尤其在高带宽成本或弱网环境下收益显著。

若服务器迟迟不响应 100(超时),规范要求客户端等待一段时间(RFC 9110 建议至少 1 秒,实现常用 1~30 秒)后决定是否直接发送请求体;多数客户端(curl、浏览器、各 HTTP 库)在超时后直接发送消息体继续请求,因为等待 100 只是优化而非必需。服务器端实现要点:对带 Expect: 100-continue 的请求要么回 100 要么回最终响应,不可悬挂;反向代理与网关需正确转发该头并处理半开上传。注意 100 Continue 是 1xx 中间响应,最终还有 2xx/4xx 结果,客户端应能同时处理两个响应阶段。

回答先讲清"先头后体、确认再传"的两阶段流程及其在"提前拒绝无效大请求"上的带宽价值,再讲超时回退策略(等固定时限后直接发体),最后补充服务端/代理的正确实现与 1xx 中间响应的处理,覆盖协议与工程两端。

#
★★

25. stale-if-error、stale-while-revalidate 与 must-understand 在源站故障时如何影响 CDN 体验,配置不当的副作用是什么?

stale-if-error、stale-while-revalidate 与 must-understand 在源站故障时如何影响 CDN 用户体验?配置不当会产生什么副作用?

  • 三个扩展指令的触发条件与行为
  • 源站故障时陈旧内容兜底的可用性价值
  • must-understand 对未知指令的保护与副作用

stale-while-revalidate 允许代理在副本过期后先返回陈旧内容给用户、同时异步回源刷新,刷新成功后更新缓存,用户无感知延迟;stale-if-error 允许在源站返回错误(5xx、超时、连接失败)时用过期副本兜底,保证源站故障期间站点仍可用。两者组合让 CDN 把"源站抖动"转化为"短暂的陈旧但可用",显著改善故障期用户体验,也是 HTTP 缓存扩展指令在 CDN 控制台中的常用配置。must-understand 则要求缓存必须理解并执行响应中的所有 Cache-Control 指令,对不认识的指令视为不可缓存(否则可能错误缓存本不该缓存的响应)。

配置不当的副作用主要有三类:其一,stale-while-revalidate 的时间窗口(如 24 小时)设置过长,且异步刷新持续失败,用户会在长时间内看到过期数据,对强一致性业务(库存、余额)造成误导;其二,stale-if-error 在源站被攻击或返回"错误但 200"(如降级空数据)时并不触发,反而掩盖故障,运维告警被陈旧内容"吸收",问题发现延迟;其三,must-understand 误配置会让缓存对未实现的指令(如较新的扩展指令)全部失效,缓存命中率骤降、回源压力激增。工程上应把 stale 窗口与业务容忍度对齐,并对"错误但 200"的降级响应单独监控。

回答先逐个讲清三个指令的语义,重点说明 stale-* 是"以陈旧换可用"的可用性手段,再分析配置不当的两面性:窗口过长造成数据过期、兜底掩盖真实故障、must-understand 误伤命中率,体现"指令是工具、边界是工程"的思考。

#
★★

26. CORS 预检(OPTIONS)为何不能被简单请求绕过,Access-Control-Request-Headers 与 Access-Control-Request-Method 如何协商?

CORS 预检(OPTIONS 请求)为什么不能被简单请求绕过?Access-Control-Request-Headers 与 Access-Control-Request-Method 是如何完成协商的?

  • 预检请求的触发条件与防护目的
  • 预检请求头与响应头的协商流程
  • 简单请求与预检请求的边界

预检的目的不是"防止跨域请求发出",而是让浏览器在真正发出可能改变数据的请求前,先向服务端确认其是否允许该跨域方法、自定义头与凭证组合,从而在浏览器侧执行 CORS 策略:若服务端未授权,浏览器直接阻止实际请求,资源不会被发送。简单请求(GET/HEAD/POST 且仅含 CORS 安全头、Content-Type 限于表单三种)不需要预检,因为这类请求不会触发复杂服务器端副作用且历史上可被表单天然发出;但"简单"只代表"免预检",不代表服务器可以忽略鉴权——浏览器仍会校验响应中的 Access-Control-Allow-Origin,服务器业务鉴权必须照常进行。

预检协商流程:浏览器发送 OPTIONS 请求,携带 Origin、Access-Control-Request-Method(实际请求将使用的方法)与 Access-Control-Request-Headers(实际请求将携带的自定义头列表);服务端响应 Access-Control-Allow-Methods、Access-Control-Allow-Headers(可回显或明确列表)与 Access-Control-Allow-Origin;若方法或头不在允许范围内,浏览器判定预检失败并阻止实际请求。响应还可带 Access-Control-Max-Age 缓存预检结果,减少重复 OPTIONS。工程要点:网关层统一放行 OPTIONS 并生成 CORS 响应头,避免业务路由拦截预检。

回答先纠正"预检可绕过"的误解:预检是浏览器侧的策略执行机制,资源在预检失败时根本不会发出;再讲清预检请求头(Request-Method/Request-Headers)与响应头(Allow-Methods/Allow-Headers)的一问一答协商过程,最后强调"简单请求免预检 ≠ 免鉴权",构成完整答案。

#

27. CORS 预检由哪些请求条件触发,简单请求并不意味着服务器可以忽略鉴权的原因是什么?

CORS 预检由哪些请求条件触发?为什么"简单请求"并不意味着服务器可以忽略鉴权?

  • 简单请求的定义与预检触发条件
  • 预检与鉴权的职责边界
  • 浏览器与服务器各自的安全职责

满足以下全部条件才属于简单请求:方法为 GET/HEAD/POST;除浏览器自动设置的头部外,仅允许 Accept、Accept-Language、Content-Language、Content-Type(且值限于 application/x-www-form-urlencoded、multipart/form-data、text/plain);不使用 ReadableStream 请求体。一旦方法为 PUT/DELETE/PATCH 等、携带自定义头(如 Authorization、X-Requested-With)、或 Content-Type 为 application/json,就会触发预检;带凭证(withCredentials)不影响预检触发但影响响应头要求。预检的目的是让服务端"预先授权"该跨域请求形态,避免实际请求被静默发出。

"简单请求免预检"只说明浏览器不需要先询问,但请求仍然携带用户凭证(Cookie、证书)且会被服务端正常处理,浏览器还会校验响应 Access-Control-Allow-Origin 决定是否暴露数据给脚本。若服务器因为"简单请求无需预检"而省略鉴权,攻击者可以构造跨站表单(POST text/plain)或图片/script 标签触发简单请求,利用用户已登录的 Cookie 完成 CSRF 或越权操作。因此服务器必须对所有请求执行身份认证、CSRF 防护与权限校验,预检只是浏览器侧的机制,与服务器鉴权完全独立。

回答分两层:先精确列出简单请求的判定条件与各类触发预检的情形,再论证"免预检 ≠ 免鉴权"——预检是浏览器策略、鉴权是服务端职责,跨站表单可发出简单请求正是 CSRF 的现实来源。逻辑闭环是本题得分点。

#

28. 浏览器跨源隔离(cross-origin isolation)开启 COOP/COEP 后 SharedArrayBuffer 与高精度计时器的可用性影响?

浏览器开启跨源隔离(cross-origin isolation)即设置 COOP/COEP 响应头后,对 SharedArrayBuffer 与高精度计时器(performance.now 精度)的可用性有什么影响?

  • 跨源隔离开启方式(COOP+COEP 组合)
  • SharedArrayBuffer 的启用条件
  • 高精度计时器降级与 Spectre 防御的关系

跨源隔离通过两个响应头开启:Cross-Origin-Opener-Policy: same-origin 让顶层窗口与同源上下文形成独立 opener 组,Cross-Origin-Embedder-Policy: require-corp 要求所有跨源子资源(iframe、脚本、图片、Worker 等)显式携带 CORP/ CORS 头才可加载。开启后页面获得两项能力:其一,SharedArrayBuffer 构造函数可用——它默认被禁用,因为共享内存是 Spectre 侧信道的重要载体;其二,performance.now() 恢复高精度(微秒级,而非降级后的 100 微秒以上粗粒度),同时 Date.now 精度也提升,因为隔离环境切断了跨源共享内存的攻击面。

未开启隔离时,浏览器为缓解 Spectre 类攻击统一降低计时器精度并禁用 SharedArrayBuffer,这是所有站点共享的代价;开启跨源隔离后,页面与外部世界隔离,精度与共享内存的威胁被限定在本站可控范围内,故浏览器允许恢复能力。工程代价:require-corp 会拦截所有未声明 CORS/CORP 的跨源资源(CDN 脚本、第三方 iframe、广告 SDK),需要逐资源补充 crossorigin 属性或 CORP 头;第三方 iframe 若不带 COEP 则可能被阻断,这是 adoption 的主要成本。Safari 对跨源隔离的支持仍有差异,需兼容降级。

回答先讲清开启方式(COOP+COEP 的职责),再解释"为什么隔离后恢复 SharedArrayBuffer 与高精度计时器"——它们是 Spectre 缓解的配套措施,最后落到工程代价(跨源资源需显式授权)与兼容性,体现对安全模型与部署成本的双重理解。

#

29. 浏览器同源策略下 cookie 的 Lax+POST 行为在跨站表单提交与 top-level GET 导航上的差异?

浏览器同源策略下,SameSite=Lax Cookie 在跨站表单 POST 提交与 top-level GET 导航上的携带行为有什么差异?Lax+POST 的中间态(Lax+POST 2 分钟规则)是什么?

  • SameSite=Lax 的携带边界(top-level GET vs 跨站 POST)
  • Lax+POST 的"2 分钟窗口"机制
  • 跨站提交场景的 CSRF 风险边界

SameSite=Lax 的默认规则:跨站 top-level GET 导航(地址栏输入、链接点击、window.open、location 跳转)会携带 Cookie,而跨站 POST 表单提交、fetch/XHR 等子资源请求一律不携带。因此 Lax 能阻断"跨站表单 POST"类 CSRF,但对"跨站链接诱导点击"类攻击(受害者被引导触发 GET 状态变更)防护有限,需要配合 CSRF Token 或服务端校验 Origin/Referer 兜底。Strict 则连 top-level GET 也不携带,安全最强但影响第三方登录跳转等正常流程。

Chrome 的 Lax+POST 中间态规则:Cookie 设置后 2 分钟内(生命周期早期),允许跨站 top-level POST 表单提交携带 Lax Cookie,目的是兼容那些"登录后立刻以 POST 跳转"的旧站点(如 OAuth 回跳、支付回跳);窗口过后恢复标准 Lax 行为。该规则是安全性与兼容性的妥协,攻击者难以利用(窗口极短且要求受害者刚设置 Cookie 即触发),但工程上不应依赖它,涉及跨站 POST 回跳的业务应显式用 SameSite=None; Secure 并配合 CSRF 防护。Safari 与 Firefox 的 Lax+POST 实现各有差异,跨浏览器测试不可省略。

回答先给出 Lax 的标准行为(GET 导航携带、POST 表单不携带)及其对 CSRF 的防护边界,再介绍 Lax+POST 2 分钟窗口这一实现细节,最后给出工程建议(不依赖窗口、跨浏览器验证),体现对浏览器安全演进的细致掌握。

#

30. HTTP/1.1 持久连接(Connection: keep-alive)与 Pipelining 为何被浏览器和服务端弃用,应用层队头阻塞、代理兼容性差与同域并发连接数限制(如 Chrome 的 6 个)之间如何关联?

HTTP/1.1 的持久连接(Connection: keep-alive)与 Pipelining 为什么被浏览器和服务端弃用?应用层队头阻塞、代理兼容性差与同域并发连接数限制(如 Chrome 的 6 个)之间是如何关联的?

  • 持久连接与 Pipelining 的机制
  • 应用层队头阻塞的产生与影响
  • 并发连接数限制与代理兼容性问题的联动

持久连接让多个请求复用同一条 TCP 连接,避免每次请求重新握手(TCP+TLS),是 HTTP/1.1 的默认行为,至今仍是基础能力;Pipelining 则在持久连接上"不等响应连续发送多个请求",期望减少 RTT 等待。但 Pipelining 有两个致命问题:其一,应用层队头阻塞——响应必须按请求顺序返回(FIFO),第一个请求是慢查询或大响应时,后续所有响应都被堵在队列后,浏览器拿到第一个响应前无法处理其他响应;其二,代理兼容性差——大量老旧代理无法正确处理乱序/部分应答,Pipelining 常被关闭,实际从未大规模可用。

由于 Pipelining 不可用,浏览器只能用"多连接 + 串行请求"逼近并发:每域并发 6 条连接(HTTP/1.1 时代 Chrome 的默认限制),每条连接内请求仍然串行,页面资源多时排队明显;连接数越多,TCP 慢启动、TLS 握手与服务器资源开销越大,因此 6 条是"并发收益"与"资源成本"的折中。三者关联:队头阻塞(Pipelining 的 FIFO 限制)→ 导致弃用 → 用多连接弥补 → 触发连接数上限 → 连接内仍串行 → 最终由 HTTP/2 多路复用(单连接内乱序交错)彻底解决。理解这条因果链是回答的关键。

本题考察 HTTP 演进的内在逻辑:先讲持久连接与 Pipelining 各自的机制与问题(FIFO 队头阻塞、代理兼容差),再解释 6 连接限制是如何作为替代方案的折中产物,最后把三者串成"HTTP/1.1 困境 → HTTP/2 出路"的因果链。