资源预加载主题封面:浅色网格上放置标题,旁侧是相连的观察、判断与行动三个框。

预加载让关键资源提前被发现

预加载在资源的普通消费者发现它之前,先告诉浏览器这个资源可能很快需要使用。由 JavaScript 创建的图片,或必须等样式表加载并解析后才发现的字体,都可能因此更早开始获取。它提供的是提前下载的机会,不能推导成所有提示资源都获得全局最高优先级,更不能保证添加之后每个页面都一定变快。 发现、获取与使用是三个阶段 现代浏览器通常能够提前扫描 HTML 中的资源,因此图片写在后面,不代表必须等主解析器逐行走到那里才开始发现;图片位于首屏之外,也不直接证明发现时间很晚。添加提示之前,应查看真实请求发起者和时间。如果资源原本就及时开始获取,额外提示可能收益很小,却与更重要的工作竞争带宽。 rel=preload 且 as=image 的 link 被处理时,会按图片目的发起获取,但不会自动插入 img 元素。同样,预加载样式表不等于应用样式,预加载脚本也不会自动执行脚本。实际消费者仍需要通过正常机制请求或使用该资源。下载提前了,却没有后续使用,只会增加开销,不能算作页面已经完成渲染。 复用依赖请求信息兼容。地址、as 表达的资源用途,以及相关的 CORS 和凭据模式,需要与最终消费者一致。字体预加载通常即使位于同源,也要配置 crossorigin,使模式匹配实际字体请求。media 适合控制条件加载,却不是每一个字体提示都必填的属性。响应式图片还需要匹配真实选择结果,不能仅凭猜测预加载最大的一张,然后假定浏览器肯定使用它。 完整性元数据又增加一条复用条件。HTML 的消费算法会比较消费者的完整性元数据与预加载条目;不兼容的元数据可能阻止消费,即使两个哈希单独看都能描述该资源。更新资产时应同步审阅完整性配置。本图片实验没有覆盖这些检查,因此不能据此断言带完整性校验的脚本预加载必定具有相同复用结果;地址匹配不是全部条件。 坏掉的样子 设计调整后,旧提示很容易留下来:页面已经移除主图,head 中的 preload 却继续下载。另一个常见问题是消费者使用不同查询参数或请求模式,导致原本期待的复用没有发生。检查时既要数请求,也要核对最终真正使用的地址。图片能够显示,只说明某次加载成功,不能证明提前下载的那份资源被复用了。 预加载过多资源,还会竞争带宽、连接和处理能力。提前发现不是暂停浏览器其他优先级规则的命令。对可选交互资源的提示,也可能让从不点击该控件的用户承担流量成本。应明确这笔取舍,并在真实受众的测试中移除成本高于收益的提示,而不是把所有资产都提前下载当作默认优化。 很快的本地实验不能建立最大内容绘制或真实响应速度已经改善的结论。性能比较需要页面、缓存状态、设备和网络条件具备可比性。资源时间记录能够说明请求何时开始,但不能单独解释整个用户体验变化,更不能把两次非受控访问的耗时差全归因于一个属性。 快速验证 把下面代码保存为 preload-image.py,使用 Python 3 执行 python3 preload-image.py。命令通过 HTTP 检查三个页面变体和一张图片的可达性,并明确输出 browser_preload_tested_by_cli 为假。服务端检查 HTML 字符串,并没有运行浏览器的资源调度过程,不能据此宣称预加载已经发生。 ...

十月 6, 2026 · 2 分钟 · XB Field Notes
以观察、判断和行动三个面板对比爬虫规则与实际 HTTP 访问。

robots.txt 引导抓取,但不授予或禁止访问

robots.txt 向实现 Robots Exclusion Protocol 的自动客户端传达抓取规则,可以减少对昂贵或无用路径的遍历,但服务器不会因此自动按照文件内容拦截请求。RFC 9309 明确区分这些规则与访问授权。应把它当作面向合作爬虫的公开接口,真正能否读取受保护资源,仍由认证和授权机制决定。配置中写了禁止抓取,不等于服务器已经建立了权限边界。 对准目标爬虫与实际路径 标准文件位于相应方案和 authority 的 /robots.txt,放在应用子目录下的同名文件,不会自动成为整个源站的策略。User-agent 分组标识规则面向的爬虫产品,通配分组按协议为其他代理提供适用规则。路径规则针对请求目标匹配,大小写不同可能产生不同结果。路由变化时应重新检查具体 URL,不能只看规则文字仍然像原来的样子。 文件应以 UTF-8 纯文本形式提供,并明确实际响应。宽泛的静态站回退规则,可能在 robots 路径返回二百和首页内容;这只证明 HTTP 已经交付,不能证明爬虫取得了预期策略。复杂的 Allow、Disallow 组合、转义和通配模式,需要对照协议及真实爬虫实现验证。一个简单解析器案例不能替整个规则集合证明所有边界行为正确。 坏掉的样子 在 robots.txt 中列出私有路径,并不会隐藏路径;公开的规则文件反而可能告诉别人它在哪里。客户端可以完全不读策略就直接请求,合作爬虫自己作出的不抓取决定,也不会转化为源站上的访问规则。确实需要限制的内容,应在请求授权的位置执行限制;Disallow 行不能替代账号权限或其他实际访问检查。公开规则和私有资源的保护条件必须分开维护。 抓取与收录也是不同问题。Google 文档说明,被禁止抓取的 URL 仍可能因为其他来源的链接出现在搜索结果里。如果爬虫无法读取页面,也可能看不到页面上希望它执行的 noindex 指令。公开内容需要按目标选择合适的收录控制,私有内容则要使用真实访问限制。不能认为阻止内容下载,就会自动移除所有已存在的搜索记录或外部引用。 策略获取失败同样有不同语义。RFC 9309 允许爬虫在文件不可用时访问资源,例如收到四开头的 HTTP 响应;如果服务器或网络错误导致文件无法到达,例如五开头的响应,爬虫起初必须假定完全禁止。规范还定义了长期失败与缓存副本的处理。部署坏了 robots 文件,可能改变抓取行为,不应只把它当成少了一份装饰性文本。 ...

十月 2, 2026 · 2 分钟 · XB Field Notes
两行标题旁边的三个相连方块,依次标注 Observe、Decide 与 Act。

CORS 决定脚本能否读取响应

跨源资源共享允许服务端选择是否向其他来源的脚本暴露响应。一个来源由协议、主机和端口组成,因此即使在同一台机器上,端口不同的两个服务也可以属于不同来源。浏览器会结合发起页面的来源检查 CORS 响应头。这套机制不会认证当前用户,也不会把接口变成只能从私有网络访问的服务。它回答的是特定页面的脚本能否获得响应内容,而不是业务操作本身是否获准。同站 Cookie 规则与同源响应访问也不是同一条边界,不能只看主机名相似,就推断两种机制都会采用相同结果。 对于允许访问的页面,接口可以在 Access-Control-Allow-Origin 中返回该页面序列化后的来源,浏览器再检查其他相关条件,决定是否暴露响应。某些公开供无凭据跨源读取的资源可以使用通配符,但通配符不能授权凭据模式为 include 的请求。服务端应根据明确规则选择允许的来源,不能把客户端任意传入的来源原样回显,就当成已经完成了访问策略判断。 分清请求送达与响应可读 一部分通常称为简单请求的操作不需要 CORS 预检。例如使用纯文本内容类型、且脚本只设置安全列表内请求头的 POST,可以满足相关条件。这里的安全列表是浏览器协议术语,并不意味着接口只读,也不意味着请求不会修改数据。普通 HTML 表单早已能够跨来源发送相似请求,而无须事先取得读取目标响应的权限,因此不能把预检有无理解成业务副作用大小。 浏览器完全可能先发送请求,收到二百状态响应,然后因为响应暴露没有获准而让脚本的 fetch 失败。此时服务端可能已经执行完操作。普通非浏览器 HTTP 客户端也不会执行这项页面来源规则。认证、授权以及防止非预期跨站操作,需要独立的服务端设计;不能依靠是否返回某个 CORS 响应头,替代对请求身份、权限和业务意图的检查。 坏掉的样子 常见故障从“浏览器已经拦截,所以服务端什么都没做”这个误判开始。用户重试一个会改变状态的请求,结果因为第一次已经成功执行而造成重复操作。重试之前应检查服务端证据,并明确业务操作自己的重复执行语义。对于依靠 Cookie 认证的状态修改,还要采用合适的跨站请求伪造防护。仅仅不返回允许来源头,不能代替判断这次修改是否应该被接受。 另一种错误,是把所有 fetch 类型错误都当成 CORS 配置问题。网络失败也会导致请求承诺被拒绝,因此要结合浏览器诊断信息、实际收到的响应头和服务端请求记录判断。把请求改成 no-cors,也不会让应用取得普通可读响应;不透明响应的限制决定了它不能成为需要消费接口数据时的通用修复方法。应修正真实的协议或权限配置,而不是只让某条错误提示消失。 快速验证 将代码保存为 cors-simple.py,运行 python3 cors-simple.py –serve,再打开打印出的本机回环页面地址。本次使用 Python 3.14.4 和 Chrome 152 检查。脚本在不同临时端口启动页面服务与接口服务,唯一的状态修改是往内存列表追加合成事件。结束后按控制键加 C 关闭两个服务。实验不涉及账号、Cookie 或生产接口,两个来源只是为了直接观察浏览器真实执行的跨源规则。 ...

九月 13, 2026 · 2 分钟 · XB Field Notes
三个相连的方块,依次标注观察、决策与行动。

CSP 明确浏览器中的执行规则

内容安全策略告诉支持它的浏览器,一个文档允许哪些动作。获取资源类指令分别控制脚本、样式、图片等类别,其他指令则负责不同边界。策略通常通过文档响应头提供,为浏览器行为增加约束,但不会修复注入漏洞、认证应用用户,也不会保证所有可能的脚本执行方式都已消失。应把它理解为具有具体作用范围的一层防护,而不是只要字段存在就获得完整安全保证。 缺失某个指令并不统一表示禁止。一些获取资源类指令会回退到默认来源指令,而嵌入父级、基础地址和表单目标等边界需要分别考虑。只包含脚本限制的策略,并不会自动完整限制图片加载、页面嵌入和表单提交。审阅时要逐项理解指令范围,尤其不能把默认来源写成全部拒绝后,就假定那些不继承该默认值的功能也被关闭。 为真正需要运行的脚本授权 随机数策略为经过明确选择的脚本元素附加不可预测的值,并在响应策略中声明同一个值。每次响应应生成新的随机数,只把它写到服务端确实准备授权的脚本上。如果自动给发现的每一个脚本元素都补上随机数,被注入的脚本也可能同时得到许可。构建时生成一次、以后持续复用的固定值,也失去了这种设计所依赖的逐响应不可预测性。 同一条来源指令中的表达式通常表示多个可选条件。把允许的主机与随机数写在一起,并不意味着脚本必须同时满足这两个条件。对于内容固定的静态页面,可以使用经过批准脚本的内容哈希,脚本精确内容发生变化时同步更新策略。需要根据页面怎样生成来选择方案;随意加入宽泛的内联执行许可,可能直接撤销原本希望通过随机数建立的限制。 坏掉的样子 开始强制执行后,合法的内联脚本、事件处理器、动态生成内容或此前没有注意到的依赖,都可能突然失效。仅报告模式能够在允许动作继续发生的同时观察违规,所以页面在该模式下正常运行,并不能证明强制策略也会允许它。报告还需要按含义解释:某条图片违规不能证明目标内联脚本被拦截,报告数量少也不能替代对关键页面功能的实际检查。 多份强制策略会共同生效。代理新增一份较宽松的响应头,并不会放宽已经存在的严格策略,因为动作必须满足每一份适用的强制策略。这可以解释为什么一个看起来正确的随机数仍然无法让脚本执行。应该检查最终交付的全部策略头和浏览器报告的有效指令,再修正真正需要的边界或合法资源用法,不能假定最后出现的字段自然覆盖此前所有限制。 快速验证 保存代码为 csp-nonce.py,执行 python3 csp-nonce.py –serve,在打印出的地址后加上 ?mode=enforce 并打开。本次检查使用 Python 3.14.4 和 Chrome 152。服务端为每份文档生成新的随机数,并故意让一个脚本没有该值。本机回环报告接收端只接收并丢弃正文,页面展示的证据来自浏览器违规事件。结束后按控制键加 C 关闭服务,不需要连接任何外部报告平台。 import base64 import json import secrets import sys from http.server import BaseHTTPRequestHandler, HTTPServer from threading import Event, Thread from urllib.parse import parse_qs, urlsplit from urllib.request import urlopen class Handler(BaseHTTPRequestHandler): def log_message(self, *args): pass def do_POST(self): self.rfile.read(int(self.headers.get("Content-Length", "0"))) self.send_response(204) self.send_header("Content-Length", "0") self.end_headers() def do_GET(self): mode = parse_qs(urlsplit(self.path).query).get("mode", ["enforce"])[0] assert mode in {"enforce", "report", "intersect"} nonce = base64.b64encode(secrets.token_bytes(18)).decode() policy = ("default-src 'none'; script-src 'nonce-" + nonce + "'; " "base-uri 'none'; object-src 'none'; report-uri /reports") body = ("""<!doctype html><meta charset="utf-8"><title>CSP nonce lab</title> <h1>Nonce, report-only, and policy intersection</h1> <pre id="result">If both scripts are blocked, this text stays unchanged.</pre> <script nonce=""" + '"' + nonce + '"' + """> window.allowed=true; window.violations=[]; document.addEventListener('securitypolicyviolation',event=>{ window.violations.push({directive:event.effectiveDirective,disposition:event.disposition,blocked:event.blockedURI}); }); </script> <script>window.unapproved=true;</script> <script nonce=""" + '"' + nonce + '"' + """> setTimeout(()=>{ window.labResult={allowed:window.allowed===true,unapproved:window.unapproved===true,violations:window.violations}; document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2); },100); </script>""").encode() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.send_header("Content-Security-Policy-Report-Only" if mode == "report" else "Content-Security-Policy", policy) if mode == "intersect": self.send_header("Content-Security-Policy", "script-src 'none'") self.end_headers() self.wfile.write(body) server = HTTPServer(("127.0.0.1", 0), Handler) thread = Thread(target=server.serve_forever, daemon=True) thread.start() origin = f"http://127.0.0.1:{server.server_port}" try: policies = [] for mode in ("enforce", "enforce", "report", "intersect"): with urlopen(origin + "/?mode=" + mode, timeout=5) as response: key = "Content-Security-Policy-Report-Only" if mode == "report" else "Content-Security-Policy" values = response.headers.get_all(key) assert len(values) == (2 if mode == "intersect" else 1) assert "'nonce-" in values[0] policies.append(values[0]) response.read() assert policies[0] != policies[1] print(json.dumps({"app_url": origin, "nonce_changes_per_response": True, "wire_policy_checks": "PASS"}), flush=True) if "--serve" in sys.argv: Event().wait() except KeyboardInterrupt: pass finally: server.shutdown() thread.join(timeout=5) server.server_close() 强制模式中,获准脚本应把 allowed 设为真,unapproved 则保持为假。相关违规事件的有效指令是 script-src-elem,被阻止的对象是内联脚本,处置方式是强制执行。将地址改为 ?mode=report 后,两个脚本标志都变为真,而违规处置方式变成报告。浏览器还可能记录其他资源违规,因此应按事件字段定位目标问题,不应把报告总数固定为一条,再据此误判执行结果。 ...

九月 10, 2026 · 2 分钟 · XB Field Notes
一个爬虫图标,旁边是分支状的 sitemap 结构。

robots.txt 与 sitemap 回答不同的问题

robots.txt 是面向爬虫的偏好;sitemap 是 URL 清单;两者都不是授权层。

七月 21, 2026 · 1 分钟 · XB Field Notes