介绍

同事发送了两版合同并让你比较它们。你把两个文件都放进比较服务,得到结果,一切看起来正常。你没有看到的是,其中一个文档包含一个指向 URL 的链接图片,而你的服务器在文件打开的瞬间就联系了该主机。输出中没有任何提示说明发生了这种情况。

这不是缺陷——这正是忠实加载文档的含义。OOXML 文件可以引用位于 Web 服务器上的图片,而不是包内部的图片,Word 和任何正确加载文档的库都会解析该引用。GroupDocs.Comparison for .NET 在 LoadOptions 上提供了两个属性,让你决定是否解析它们:SkipExternalResources 和 WhitelistedResources。

这两个属性组合提供了三种配置,本文将比较这三种——宽松的默认设置、阻止所有外部资源、以及阻止所有外部资源但允许特定引用。阅读完后,你将知道在不同文档来源下该选哪一种,以及导致这些设置看似无效的两个常见错误。

💡 完整可运行示例:block-external-resources-on-document-load-dotnet —— 一个可运行的控制台项目,它自行提供被引用的图片并记录每一次请求,方便你观察每个设置的实际效果。

外部引用隐藏的位置

在选择设置之前,先了解你要控制的内容很重要。.docx 文件的外部引用存在于两个不同的地方,而且这两处都不在文档正文中显示,容易被忽视。

  • 第一处是 word/_rels/document.xml.rels 中的关系(relationship),其 TargetMode="External" 并包含绝对 URL。图片在正文中以绘图的形式出现,指向该关系的 ID,因此 URL 本身从未出现在受影响的内容附近。

  • 第二处是正文中的 INCLUDEPICTURE 域代码(field code),其 URL 被写在域指令内部。Word 在页面渲染时解析它;比较库在文档加载时解析它。

这两种机制都会遵循下面讨论的两个加载选项,这一点很重要,因为文档可能同时使用这两种方式。你在关系文件中看到的引用并不意味着文档中没有另一个字段代码引用。

方法 1:默认行为——解析引用

SkipExternalResources 默认值为 false,因此在未做任何配置的情况下加载文档时会解析远程引用:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = false
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

这提供了最高的保真度:比较的文档会包含它们所引用的所有内容,完全等同于 Word 的渲染方式。对于你自己的应用或模板生成的文档(所有引用 URL 都指向你自己维护的基础设施),这是正确的选择——缺失的链接图片会导致比较结果误导。

代价是每个引用都会被请求,无论是谁放进去的。还有一个与信任无关的时间成本:如果引用的 URL 已经失效,加载过程会等待完整的连接尝试,在每一次比较中都如此。

方法 2:阻止所有外部资源

只需将一个属性设为 true,即可关闭该文档的远程引用解析:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = true
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

不会发出任何请求。被引用的图片会从结果中缺失——这点必须说明清楚——除此之外没有其他变化。该设置只决定加载什么,而不影响差异检测方式,因此两份文档之间的文本和结构变化仍会被准确发现。唯一失去的功能是检测 引用图片内部 的变化,因为图片根本没有被加载。

这是一种适合作为基线的配置,适用于你没有创建的文档:Web 应用的用户上传、电子邮件收到的文件、在构建代理上进行的比较(通常不希望发出外部请求)。它是“全有或全无”的策略——如果你真的需要的链接图片也会被阻止,结果中只会缺少该图片,且不会有任何提示。

方法 3:阻止所有外部资源,但允许特定引用

第三种配置是最细致的。WhitelistedResources 接受 List<string>,仅在 SkipExternalResources 为 true 时生效:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = true,
    WhitelistedResources = new List<string> { "includepicture-field.png" }
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

列表中的条目是 URL 片段,而不是文件名。每个片段会与引用的完整 URL 进行匹配,只要在 URL 任意位置出现即视为匹配并允许该资源加载。这正是白名单可移植的原因:"includepicture-field.png" 能匹配任何协议、主机和路径,只要 URL 包含该片段,因此同一列表在开发和生产环境下均可使用,无需改写。

同样的属性也可能产生相反的效果。过短或过于通用的片段——如 logo.png,甚至更糟的 .png——可能匹配到你根本不想允许的引用。请挑选足够具体的片段,以唯一标识你真正想允许的资源。

在示例引用中,此配置会加载白名单中的图片,而对第二个未被列入的引用进行阻止。请求日志显示:宽松默认会产生五次请求,而此配置只产生三次,并且仅记录白名单文件的请求。

应该使用哪种配置?

根据文档的来源匹配相应的设置。你自己的应用或模板生成的文档可以保留默认,因为所有引用 URL 都指向你已经运营的基础设施。任何来自外部的文档——用户上传、电子邮件附件、第三方文件——都应使用 SkipExternalResources = true。仅在确实需要解析某个受信任引用时,才添加一个狭窄的 WhitelistedResources 片段。

三种配置对比

关注点 默认 阻止全部 阻止 + 白名单
需要设置的属性数 0 1 2
出站请求 所有引用 无 仅白名单
每个引用的控制 否 否 是
死链接导致的加载时间 是 否 仅白名单
最适合的场景 你自己生成的文档 来自任何外部的文档 在不可信内容中需要可信模板的情况

决策依据是文档的来源而非性能。你系统生成的文档可以保留默认;外部文档则应阻止。只有在特定引用必须解析时才使用白名单——例如公司模板从内部 URL 拉取页眉图片,而报告作者则随意粘贴图片。

两个常见错误

这两种错误会导致相同的症状:你设置了选项,却看起来没有任何效果。

仅设置白名单而未打开开关。 WhitelistedResources 仅在 SkipExternalResources 为 true 时被检查。单独设置白名单等于什么也不做——因为没有阻止的对象可供例外。如果发现白名单似乎被忽略,请先检查此项。

仅在源文档上设置选项。 这是更隐蔽的错误。加载选项描述的是单个文档的加载方式。Comparer 构造函数接受源文档的选项;每一次 Add() 调用接受对应目标文档的选项:

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

如果只把选项传给构造函数而忘记在 Add() 中传入,那么只有源文档受到保护,所有目标文档仍会自行请求其引用。比较能够成功,结果看起来合理,但仍有一半的文档在背后访问网络。若源文档和目标文档需要不同的处理方式,请为它们分别创建 LoadOptions 实例——这正是 API 设计为每个文档单独接受选项的原因。

验证是否真正生效

被阻止的资源几乎不留痕迹。输出文档缺少图片,看起来就像原本就没有该图片的文档。因此,仅仅检查结果文件并不是确认设置是否生效的好方法。

应该监视提供资源的一端。示例项目有意采用了这种做法:它在一个空闲的回环端口上启动一个小型 HTTP 监听器,生成指向该端口的演示文档,并记录每一次请求的次数。结果显示:五次请求 → 零次 → 三次。对真实文档来源进行网络抓包,同样可以获得相同的信心。

结论

三种配置,一条决策规则:对你自己生成的文档保留默认,对其他所有文档使用 SkipExternalResources = true,仅在确实需要解析特定受信任引用时使用狭窄的 URL 片段白名单。

随后检查两件会悄悄抵消设置效果的事——没有 SkipExternalResources = true 的白名单,以及只传给 Comparer 构造函数而未传给每个 Add() 调用的选项——并且从提供资源的一端而非输出文件进行验证。

其他资源