文档
/
实用方案
/

宿主端模板替换

宿主端模板替换

Pro
v6.10.0+

将服务端渲染的值(例如 Go 模板风格的 `{{ .Field }}` 占位符)注入 VM 混淆后的 JavaScript,而不破坏混淆流程。

问题

您的后端(Go、Rails、Django、PHP 等)提供一个 JavaScript 文件,其内容必须按请求部分渲染:一个 API 端点、一组功能开关、一段初始状态数据、一个构建 ID 或一个 nonce。您用 vmObfuscation: true 混淆这段 JS,但同时还需要模板引擎在混淆完成之后替换混淆输出中的占位符。reservedNames 和 reservedStrings 能让占位符在输出中保持可见,从而可以被替换。没有它们,字符串占位符会被吸收进 VM 字节码,标识符占位符则会在多个被改写的位置输出,于是模板引擎要么找不到可替换的内容,要么会破坏输出。

考虑改为以数据形式加载这些值

如果可以,最好完全不修改受保护的输出:在受保护的包运行之前,以数据形式加载运行时配置。把它放在单独的脚本中,或从经过身份验证的端点获取。这样受保护的输出保持不变,vmSelfDefending 也可以继续启用。无论哪种方式,发送到浏览器的数据对该浏览器都是可见的。

JavaScript

当这些值确实必须替换进混淆后的文件时,请使用下面的模式。

关于 Go 示例的说明: 它们对混淆后的输出做的是 纯字符串替换(strings.ReplaceAll / strings.NewReplacer),并自行处理转义,每种模式中都有展示。它们没有经过 Go 的 html/template 包处理,否则该包会在此基础上再套用它自己的上下文相关 JavaScript 转义,导致载荷被双重转义。如果您确实通过 html/template 渲染,请去掉手动转义,让模板引擎只转义一次。

该选择哪个选项?

服务端的值是…使用
一个原始 JS 值(数组、对象、数字、布尔值,即任意 JSON 值)模式 1 - reservedNames + 标识符占位符
一个字符串(常见情况:渲染后的 JSON、构建 ID、nonce)模式 2 - reservedStrings + 模板字面量占位符
一个字符串,且代码必须保持 ES5,或周围的 API 需要带引号的字符串字面量模式 3 - reservedStrings + 带引号字符串占位符

混淆后替换与 vmSelfDefending: true 不兼容。请参阅本方案末尾的兼容性说明。

模式 1:reservedNames 配合标识符占位符

最适合:注入原始 JS 表达式(数组、对象、数字等),完全无需担心引号转义。

选择一个绝不会与真实代码冲突的独特标识符,直接引用它,并在 reservedNames 中列出匹配它的正则表达式。在 VM 混淆下,该标识符会经由保留表达式数组处理,并原样出现在输出中,例如:

JavaScript

用一个完整的、经 JSON 序列化的表达式替换该标识符。不要把值放进引号或反引号中:该标识符并不在字符串字面量内,因此注入的值会被 JS 引擎当作普通表达式解析。请使用可信的序列化器;当脚本嵌入 HTML 时要转义 <;并测试包含引号、反斜杠、换行、反引号和 ${...} 的值。

JavaScript

模式 2:reservedStrings 配合模板字面量占位符

最适合:Go / Jinja 风格的模板引擎,它们要求使用 {{ .Field }} 这类分隔符,而这些分隔符用反引号包裹后恰好是合法的 JS 文本。

占位符位于一个只有单个 quasi、不含插值的模板字面量中。在 VM 混淆下,它会经由保留表达式数组处理,并在输出中保留原始的反引号形式。

源代码

JavaScript

混淆器选项

界面

要保留上面源代码中的 {{.Config.FeatureFlags}} 占位符,请把下面的正则表达式添加到 Reserved Strings 字段中,使用单个反斜杠。界面会原样存储该值,因此与 JS 代码中不同,反斜杠无需双写:

文本

混淆器界面中的 Reserved Strings 字段,其中的正则表达式使用单个反斜杠输入

API

JavaScript

混淆后

JavaScript

占位符被逐字节保留,包括两侧的反引号。

宿主端替换

把 {{.Config.FeatureFlags}} 替换为一个已针对模板字面量转义的 JSON 字符串。反引号内部的 " 无需转义,但载荷中的反斜杠、反引号或 ${ 仍会改变或提前结束该字面量,因此要转义这三者:

代码

运行时:JSON.parse(`{"newCheckout":true,"darkMode":false}`),可以正常工作。

对于字符串值,为什么优先使用模板字面量而不是单引号/双引号?反引号不需要转义 JSON 载荷中的 "。这一点很重要,因为大多数服务端渲染的值都是 JSON,而 JSON 中充满了双引号。使用双引号占位符时,您需要转义每一个内部的 "(参见模式 3);使用反引号时,载荷只需转义少见的反斜杠、反引号和 ${。

模式 3:reservedStrings 配合带引号的字符串占位符

最适合:必须保持为 ES5(不能使用模板字面量)的源代码,或周边 API 需要普通字符串字面量的场景。

占位符是一个单引号或双引号字符串字面量。在 VM 混淆下,它会经由保留字符串数组处理,该数组以 JSON.stringify 序列化的 JS 数组形式输出,无论输入使用哪种引号,输出始终是双引号。

源代码

JavaScript

混淆器选项

界面

要保留上面源代码中的 {{.Page.Tags}} 占位符,请把下面的正则表达式添加到 Reserved Strings 字段中,使用单个反斜杠。界面会原样存储该值,因此与 JS 代码中不同,反斜杠无需双写:

文本

混淆器界面中的 Reserved Strings 字段,其中的正则表达式使用单个反斜杠输入

API

JavaScript

混淆后

JavaScript

宿主端替换

由于占位符位于 JS 双引号字符串内,注入的 JSON 载荷必须转义其中的反斜杠和内部的 " 字符:

代码

输出结果:

JavaScript

运行时:JSON.parse("[\"news\",\"tech\",\"release\"]") → ["news","tech","release"]。

如果忘记转义,浏览器会看到不成对的引号并抛出 SyntaxError。模式 2 使用反引号,完全避开了双引号问题。

同一程序中的多个占位符

三种模式可以组合使用。单个带有分支结构的 reservedStrings 正则表达式,就能匹配模板引擎输出的所有占位符形式,这里同时匹配 {{ .Field }} 和 %{ .Field } 两种风格:

JavaScript

您可以在同一份源代码中混用标识符占位符(用于原始 JS 值)和字符串占位符(用于渲染后的 JSON),并根据服务端实际要注入的内容为每个占位符分别选择。

兼容性说明

vmSelfDefending 会破坏混淆后的替换

VM Self Defending 会检测到构建完成后对混淆输出的任何改动,包括合法的模板替换,随后受保护的代码将拒绝运行。

如果您依赖宿主端模板替换,请设置 vmSelfDefending: false。同时也要关闭 selfDefending:它在 VM 混淆下不起作用,但在不使用 VM 时,它同样禁止对输出做任何修改。

占位符提示

  • 不要在标识符和字符串之间复用占位符。像 __TOKEN__ 这样的保留名称,与匹配 __TOKEN__ 的保留字符串,对应的是两条不同的代码路径(保留表达式数组与保留字符串数组)。请为两者使用不同的文本形式,例如标识符占位符采用 UPPER_SNAKE 约定,字符串占位符采用分隔符包裹的形式({{ ... }}、%{...}、<<<...>>>)。这样,一个正则表达式中的错误就不会静默地匹配到另一类占位符。
  • reservedStrings 正则表达式针对原始字符串值进行匹配。正则表达式匹配的是字符串的运行时值,而不是源代码文本。\{\{[^}]+\}\} 会匹配包含 {{.something}}(或任何其他 {{...}} 形式)的字符串。如果您的占位符可能被额外内容包裹("prefix-{{.Field}}-suffix"),正则表达式仍会匹配,但整个字符串都会被保留,请据此规划替换逻辑。
  • 适用于任何模板引擎。虽然示例使用的是 Go 语法,但 javascript-obfuscator 集成中没有任何内容是 Go 专属的。任何能对混淆器输出做字符串级替换的工具都可以使用:Rails ERB、Django、PHP 短标签、CI 流水线中的 sed 等。请选择您的引擎天然输出、且不会与真实 JS 语法冲突的分隔符。