sub_filter_once off 是全局替换的前提,因默认 sub_filter_once on 仅替换首个匹配项;需配合 sub_filter_types 指定类型、禁用 gzip 或启用 gunzip、确保 Content-Type 正确且响应未超 buffer 限制。

为什么 sub_filter_once off 是全局注入的前提
因为 Nginx 默认只对响应体做一次子串替换(sub_filter_once on),遇到第一个匹配就停止。如果页面里有多个 <head> 或多个 </body>,或者你想在多个位置插入不同内容(比如关键词 + 监控脚本),必须关掉这个限制,否则只有第一个位置生效。
关键点在于:sub_filter_once off 不是“开启多次替换”,而是“允许连续匹配并替换所有符合规则的片段”——它依赖正则锚点和上下文边界来准确定位插入点。
- 必须配合
sub_filter的正则模式使用(sub_filter_types *+sub_filter带~或~*) - 响应体必须是文本类型(
text/html、application/json等),二进制流(如图片、字体)会被跳过 - 若上游返回了
Content-Encoding: gzip,Nginx 不会解压再过滤,直接跳过 —— 必须确保后端关闭 gzip,或用gunzip on解压后再过滤
sub_filter 插入第三方监控脚本的写法要点
不能直接写 sub_filter "</body>" "<script src='//cdn.example.com/monitor.js'></script></body>",这样会破坏 HTML 结构(比如把 </body> 替换成含 </body> 的字符串,导致嵌套闭合)。
正确做法是用非贪婪正则锚定位置,并保留原始标签:
sub_filter_types text/html; sub_filter_once off; sub_filter '</body>' '<script src="//cdn.example.com/monitor.js"></script></body>' ig;
-
ig标志保证大小写不敏感且全局匹配(g在 off 模式下才真正生效) - 必须确保
sub_filter_types包含text/html,否则对 HTML 响应完全不触发 - 若页面用了 CSP 且未声明
script-src允许该 CDN,脚本会被浏览器拦截 —— 注入成功 ≠ 执行成功
动态注入网页关键词:别只靠简单字符串替换
想把页面中所有出现的 “旧品牌名” 替成 “新品牌名”,看起来简单,但容易误伤:
- 替换
"旧品牌名"→"新品牌名",可能把"旧品牌名有限公司"变成"新品牌名有限公司"(合法),但也可能把"旧品牌名API"变成"新品牌名API"(语义错误) - 更危险的是:在
<script>或<style>块里也替换,导致 JS 报错或样式失效
稳妥做法是限定上下文,例如只替换 HTML 文本节点中的独立词(需结合正则边界):
sub_filter '\b旧品牌名\b' '新品牌名' ig;
但注意:\b 在 UTF-8 中对中文无效;中文场景建议用 (?<=[>\s])旧品牌名(?=[<\s]) 这类环视,前提是启用 PCRE 正则(Nginx ≥ 1.19.0 支持 ~* 中的环视)。
常见失败原因:你看到的响应根本没走 sub_filter
最常被忽略的三点:
- 后端设置了
Cache-Control: no-cache或Expires过期时间很短,但 Nginx 开了proxy_cache—— 缓存响应体是原始未过滤版本,sub_filter只作用于实时响应,缓存里不会重过滤 - 响应头带
Content-Length,而sub_filter修改内容后长度变化,Nginx 不会自动重算该头,导致浏览器截断或解析异常(需加sub_filter_last_modified off并禁用Content-Length,或用chunked_transfer_encoding on) -
sub_filter对 304 响应、空响应体、HTTP/2 流优先级调整后的分块响应不生效 —— 它只处理完整、可缓冲的文本响应体
调试时先用 curl -I 看响应头是否含 X-Sub-Filtered: yes(可自定义添加),再确认 Content-Type 和编码状态,比反复改正则更省时间。

















