内容


XML 规范化形式简介

让 XML 适应回归测试、数字签名等

XML 的传统来自文档世界,这在其语法规则中也得到反映。与数据库记录的数据格式相比,它的语法更加宽松。XML 解析器将 XML 文档的编码形式(编码在 XML 声明中规定)转化成表示 XML 文档信息的抽象模型。W3C 将这种抽象模型形式化为 XML Infoset(请参阅 参考资料),但是许多 XML 处理必须考虑已编码的源代码形式,这种形式允许存在多种不同的词法格式:属性的顺序没有规定,元素名及其属性间的空白字符的使用规则也很灵活,并且有多种不同的表示字符和转义特殊字符的方法等。名称空间又带来了更大的词法灵活性(比如是否使用前缀是可选的)。结果造成您拥有许多按照 XML 1.0 规则完全等价的文档,但在对编码的源文件进行逐字节比较时,它们可能完全不同。

词法上的灵活性在诸如回归测试和数字签名等方面带来了一些问题。假设要创建一个包含案例的测试集,想以清单 1 中的文档作为正确的输出结果。

清单 1. 示例 XML 文档
<doc>
  <a a1="1" a2="2">123</a>
</doc>

如果要进行严格意义上的 XML 测试,那么您应该认识到,应该将清单 2 中的文档也看作是一个正确的输出。

清单 2. 清单 1 中 XML 文档的等价形式
<?xml version="1.0" encoding="UTF-8"?>
<doc>
  <a
     a2="2"   a1="1"
  >123</a>
</doc>

标签之内的空白不同,属性的顺序也改变了,而且字符实体被换成了等价的文字字符,但毫无疑问的是,Infoset 是完全相同的。通过逐字节的比较来建立这种等同性是很难的。如果希望保证通过消息系统发送的文档在传递过程中没有被破坏或篡改,那么就要用到数字签名。为此,需要使用加密的散列值或者完善的文档数字签名。但是,如果通过消息系统发送 清单 1,经过常规的处理之后,清单 1 有可能变成 清单 2。这样的话,简单的散列值或者数据签名就无法匹配,尽管文档实质上并没有被修改。

解决这一问题的 W3C 解决方案已经作为 XML 数字签名规范的一部分开发出来了。W3C 定义了 规范化 XML(请参阅 参考资料),这是 XML 的规范化词法形式,去掉了所有允许的变化,采用了严格的规则来进行逐字节比较。转化成规范形式的过程称为 规范化(通常缩写为“c14n”)。本文将介绍 XML 规范形式。

规范化形式规则

c14n 过程的最佳概括就是规范中提供的列表(我做了一些修改),如下所示:

  • 文档用 UTF-8 编码。
  • 解析之前输入中的断行规范化为“#xA”。
  • 像验证解析器那样将属性值规范化。
  • 像验证解析器那样向元素中添加默认属性。
  • 将 CDATA 节替换为它们的文字字符内容。
  • 字符和已解析实体引用替换为文字字符(特殊字符除外)。
  • 属性值和字符内容中的特殊字符替换为字符引用(与一般的结构良好的 XML 相同)。
  • 删除 XML 声明和 DTD。(注意:通常我建议使用 XML 声明,但是赞同在规范化 XML 形式中忽略 XML 声明的理由。)
  • 将空的元素转化成起始-结束标签对。
  • 规范化文档元素之外和起始、结束标签之间的空白。
  • 保留字符内容中的所有空白(不包括换行规范化过程中删除的字符)。
  • 属性值分隔符设为引号(双引号)。
  • 删除各元素中多余的名称空间声明。
  • 对名称空间声明和每个元素中的属性采用词典序。

不必担心这些规则是否有点不清楚,后面还将详细地解释,并通过例子说明实际应用的更一般的规则。本文中不讨论 c14n 步骤中涉及到 DTD 验证的部分。我曾经多次提到 XML Infoset,但有意思的是 W3C 没有选择在 Infoset 的基础上定义 c14n,而是按照 XPath 数据模型(一种比 Infoset 更简单的数据模型,有人说该模型更清晰)来定义 c14n。这可能是个无关轻重的细节,不会影响您对规范化形式的理解,但是如果需要使用基于 Infoset 的技术,则有必要记住这一点。

规范化标签

标签规范化需要在标签中应用特定的空白规则、特定顺序的名称空间声明和规整的属性。下面是我自己总结的建立规范化起始标签的非正式的步骤:

  1. 左尖括号(<),后面跟着元素 QName(前缀加冒号和本地名)。
  2. 如果有默认名称空间声明的话紧跟在后面,然后是其他名称空间声明,按照定义前缀的字母顺序。省略所有多余的名称空间声明(已经在祖先元素中声明而且没有被改写的名称空间)。每个名称空间声明之间有一个空格,等号和包含名称空间的 URI 两侧没有空格。
  3. 所有的属性按照字母顺序排列,前面有一个空格,等号、包含属性值的双引号两侧没有空格。
  4. 最后是右尖括号(>)。

规范形式的结束标签很简单:左尖括号(<)、元素 QName 和右尖括号(>)。清单 3 是一个非规范形式的 XML 例子。

清单 3. 非规范形式的 XML 的例子
<?xml version="1.0" encoding="UTF-8"?>
<doc xmlns:x="http://example.com/x" xmlns="http://example.com/default">
  <a
     a2="2"   a1="1"
  >123</a>
  <b y:a1='1' xmlns="http://example.com/default" a3='"3"'
     xmlns:y='http://example.com/y' y:a2='2'/>
</doc>

清单 4 是同一文档的规范形式。

清单 4. 清单 3 的规范形式
<doc xmlns="http://example.com/default" xmlns:x="http://example.com/x">
  <a a1="1" a2="2">123</a>
  <b xmlns:y="http://example.com/y" a3=""3"" y:a1="1" y:a2="2"></b>
</doc>

规范化 清单 3 需要做以下修改:

  • 删除 XML 声明(文档已经采用 UTF-8 编码,不需要转换)。
  • doc 上的默认名称空间声明放在其他名称空间(该例中前缀为 x 的名称空间)的前面。
  • 减少 a 起始标签中的空格,每个属性之前只留下一个空格。
  • 删除 b 起始标签中多余的默认名称空间声明。
  • 确保其余的名称空间声明(对于 y 前缀)出现在其他所有属性的前面。
  • 其他属性按照它们的 QNames(如“a3”、“y:a1”、“y:a2”)以字母顺序排列。
  • xmlns:y 名称空间声明、属性 y:a1y:a2a3 上的分隔符从单引号( ')改为双引号( "), a3 还需要将嵌套的双引号( ")转义成 "

我使用了 Python c14n 模块测试规范形式的转换,该模块来自 PyXML (请参阅 参考资料)。清单 5 是用来将 清单 3 规范化成 清单 4 的代码。

清单 5. 规范化 XML 的 Python 代码
from xml.dom import minidom
from xml.dom.ext import c14n
doc = minidom.parse('listing3.xml')
canonical_xml = c14n.Canonicalize(doc)
print canonical_xml

规范化字符数据

规范形式的字符数据基本上就是尽量使用文字:字符实体被解析为原始的 Unicode(然后序列化为 UTF-8);CDATA 区段则用原来的内容替换;沿着这条路进行其他的替换。这种方法同样适用于属性值和内容中的字符数据。还将按照 DTD 类型规则规范化属性,但这主要与使用 DTD 的文档有关,本文不再讨论。清单 6 中的示例文档部分参考了 c14n 规范中的一个例子。

清单 6. 字符数据规范化的 XML 例子
<?xml version="1.0" encoding="ISO-8859-1"?>
<doc>
   <text>First line
Second line</text>
   <value>2</value>
   <compute><![CDATA[value>"0" && value<"10" ?"valid":"error"]]></compute>
   <compute expr='value>"0" && value<"10" ?"valid":"error"'>valid</compute>
</doc>

清单 7 是规范形式的同一文档。

规范化 清单 6 需要做以下修改:

  • 删除 XML 声明并转化为 UTF-8 形式。
  • 将字符引用 2 改为实际的数字 2。
  • CDATA 节替换为内容,并使用 > 转义右尖括号(>),用 & 转义“与”符号(&),用 < 转义右尖括号(<)。
  • 用双引号代替 expr 属性中所用的单引号,然后将双引号( ")字符转义成 "

清单 6 和清单 7 都没有涉及到的一个重要步骤是到 UTF-8 的转化,因为这不方便用清单来说明。假设源文档内容中包含字符引用 ©(表示版权符号),规范形式将使用 UTF-8 序列来代替(十六进制字节 C2 和 A9)。

结束语

有时候真正需要的可能是签署或比较 XML 文档的一个子树,而不是全部内容。可能您只想签署 SOAP 消息主体而忽略信封。W3C 在 专用规范形式的规范中提供了这种机制,它基本上是对目标子树内外的名称空间声明进行排序。

我提到了前缀选择造成的各种可能的变化。XML Namespaces 规定前缀是无关紧要的,因此只有名称空间前缀不同的两个文件应视作同一文件。不幸的是,c14n 没有考虑这种情况。某些完全合法的 XML 处理操作可能修改前缀,因此要注意这类潜在的问题。

Canonical XML 是一个需要牢牢掌握的重要工具。您可能不会马上遇到与 XML 有关的安全或软件测试问题,但是一旦熟悉这些领域您会奇怪为何经常需要 c14n。它是帮助您清除最初没有注意避免的潜在问题的工具之一。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=49321
ArticleTitle=XML 规范化形式简介
publish-date=12012004