IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML  >

使用 XML: 安全编码实践,第 1 部分

避免常见的 XML 错误

developerWorks
文档选项

未显示需要 JavaScript 的文档选项

讨论


级别: 中级

BenoÎt Marchal (bmarchal@pineapplesoft.com), 顾问, Pineapplesoft

2005 年 5 月 30 日

BenoÎt 检查了自己的项目笔记,整理了一份常见 XML 技术陷阱列表。在项目中研究这些潜在的问题可以避免很多挫折。本系列文章共有四篇,这是第一篇,BenoÎt 分析了 XML 语言本身的风险。

过去七年来,作为一名顾问、培训人员和作者,我有幸从一个特殊的角度见证了 XML 的发展和成熟。

XML 最初出现的时候,组织和开发人员对这种新的“标记语言——不管它是什么”彬彬有礼地保持怀疑态度。后来,随着使用 XML 解决的问题越来越多,他们变得热情起来。现在,很多开发人员和组织很自然地将 XML 纳入他们的项目之中。

不幸的是,应用的增加带来了滥用,从这个意义上说,XML 也反映了其他技术的采用过程。任何新技术的第一批用户通常都满怀热情(如果要让同事和客户承认这项技术的价值就必须如此),但是他们可能也有怀疑,因此通常会花费时间来研究如何最好地实现这种新技术。

随着技术的成熟,不断地得到承认。当这项技术在越来越多的应用程序中使用时,出现的错误也越来越多。所幸的是,同时经验也积累起来了:针对普遍问题经过测试的解决方案以及常见的陷阱出现并经过整理。

为了撰写这四篇文章,我翻阅了自己的笔记查找反复出现的 XML 陷阱。我希望将其整理出来并给出解决的方法,帮助您避免成为这一技术常见问题的牺牲品。

首先从最基本的一层——XML 本身开始。坚持共同的语法是建立可靠应用程序的第一步。这一部分讨论三个常见的问题:

  • 解析器和字符转义的使用
  • 编码
  • 名称空间

后续文章将探讨如何可靠地使用 XML 文档、如何验证和测试 XML 文档、如何建立 XML 和其他文件格式的接口,如图像、视频、字处理等等。

文雅的语法

第一节介绍 XML 语法的一些基本知识。如果对此已经非常精通,完全可以跳过这一节。

XML 语法很简单:最基本的一点就是开标签和闭标签必须匹配。但是我希望以后不会再受到这样的电子邮件,“我尝试用这样那样的工具处理附件中的 XML 文档,但是行不通,还有其他的工具吗?”毫无例外的,我打开文档后总能找到明显的语法错误,比如没有反斜杠的空标签(应该是这样: <empty/>)。如果文档不完全符合 XML 语法,就不是一个 XML 文档;如果不是 XML 文档,XML 工具就不能处理它。XML 有一种非常精确而正式的语法。一个文档要么完全符合语法,要么不被看作是 XML 文档。仅此而已。

相反,有些应用程序可能不认可无安全有效的文档。应用程序可能没有完全实现该语法,因而无法识别,比方说字符实体(如 î)。

XML 的问题时看起来太简单。编写某个东西常常看起来比学习另一个组件更容易、更快捷。在封闭的环境中,应用程序读取自己生成的文档,这种方法可能奏效,但是在多个应用程序使用文档的产品环境不大可能。

解决之道

所幸的是,使用 XML 解析器很容易避免这种问题。每种编程语言都由适用的 XML 解析器(即使 Cobol 都有强大的 XML 支持),没有理由不使用。

作为开发人员,您有两种选择:XML 解析器或者编组组件。如果需要底层控制 XML 文档的解码,应该使用 XML 解析器。对于本文而言,解析器使用 DOM、JDOM、SAX 或 StAX 都没有关系,但真正的 XML 解析器是正确读取 XML 文档的唯一保证。

如果不需要更多地控制解析过程,可以找到更方便的编组组件,如 JAXB、Castor 或 Axis。编组组件直接在 XML 标签和 Java™ 对象之间映射。 JAXB 和 Castor 是为了处理文件中的文档设计的,Axis 用于 Web 服务。编组组件内嵌有 XML 解析器,可以确信它们完全实现了语法。

虽然建议使用解析器读取 XML 文档,如果自己实现了写文档例程,也可以避开解析器。读取 XML 文档是一项复杂的任务,因为读者必须支持完整的语法,但是写 XML 文档相对容易一些,因为可以避开语法的一个子集:如果不需要属性,就不需要支持属性;如果不需要多种编码,就不需要支持多种编码,依次类推。

这里唯一的陷阱是需要正确地转义保留字符(参见表 1)。特别要注意实体字符(如 î),因为它们依赖于文档编码(请参阅“编码的问题”)。

表1. 保留

字符 转义序列 说明
<<  
&&  
>>  
''仅用于属性,如果使用 " 作为分隔符
""仅用于属性,如果使用 ' 作为分隔符
其他&#unicode;当前编码中不支持的任何字符

类似 清单 1 所示的一个简单循环通常就足够了。更有效的实现该功能是可能的,但如果写入 UTF-8 或 UTF-16 流,清单 1 在语法上是有效的(否则,还需要将某些字符转义成字符实体)。


清单 1. 繁琐的转义实现
				// assumes UTF-8 or UTF-16 as encoding,
public String escape(String content)
{
   StringBuffer buffer = new StringBuffer();
   for(int i = 0;i &lt; content.length();i++)
   {
      char c = content.charAt(i);
      if(c == '&lt;')
         buffer.append("&amp;lt;");
      else if(c == '&gt;')
         buffer.append("&amp;gt;");
      else if(c == '&amp;')
         buffer.append("&amp;amp;");
      else if(c == '&quot;')
         buffer.append("&amp;quot;");
      else if(c == '\&apos;')
         buffer.append("&amp;apos;");
      else
         buffer.append(c);
   }
   return buffer.toString();
}

有些开发人员更喜欢 CDATA 节而非转义。CDATA 是表明某一部分文档可能包含非转义保留字符的一种机制。比如,<condition><![CDATA[a > 4]]></condition>。本系列的第三篇文章中,我还将讨论 CDATA 节,现在只要知道它们比转义更安全就足够了,因为 CDATA 节不能包含另一个 CDATA 节。

更灵活的解决之道是求助于转换程序,请参阅我的技巧文章“Implement XMLReader”(developerWorks)。

其他方法

如果必须和违反 XML 语法的应用程序打交道,又无法说服开发人员修改他/她的应用程序,怎么办呢?

我发现,更好的办法是认为这类应用程序根本不生成 XML,增加一个步骤将这种不正常的 XML 转化成正确的 XML。为什么要增加一个步骤呢?因为这样可以隔离不符合标准的地方,能够使用任何 XML 工具进行后续的处理。





回页首


编码的问题

编码的使用可能带来更严重的问题。开发人员常常忽视了一个问题,编码没有限制 XML 支持的字符集。每个 XML 文档都支持完整的 Unicode 字符集(XML 1.1 中是 16-bit 或 32-bit 字符)。

对 XML 文档编码可以缩小文档的大小,但是并没有限制文档使用特定的 Unicode 子集,这要感谢字符实体。事实上,通过字符实体,可以插入 Unicode 编码表中的任何字符,即使文档是用最严格的编码(US-ASCII,只适合四种语言:English、Hawaiian、Latin 和 Swahili)。

这是一个问题,因为 Java 应用程序或者最新版本的 DB2® 可能支持 Unicode,但是很少有遗留系统也支持 Unicode。因此如果 XML 流进入遗留应用程序,可能需要解决 Unicode 的问题。为了避免误解,我们再说明一次,强制采用某种编码不能解决问题,因为如上所述,可以将特殊字符转义成字符实体。

因为很少有可能重写遗留系统,就需要某种转换例程将 Unicode 字符转化成应用程序能够接受的字符集,比如将“î”转化成直接的“i”(去掉抑扬符号)。多数 XML 解析器都提供了操纵 Unicode 字符的例程。





回页首


名称空间问题

本文中第三个也是最后一个问题的来源是 XML 名称空间。

引入名称空间是为了管理 XML 词汇表,避免标签同名。不同上下文中的两个词汇表常常使用同一个标签。比如,消息词汇表中可能包含 subject、date、from、to 和d body 这类标签(参见 清单 2),而数字资产词汇表可能包含 subject、date、description、camera 和 frame number 这类标签(参见清单 3)。


清单 2. 消息词汇表
				
<envelope>
   <subject>Test memo</subject>
   <date>April 26, 2005</date>
   <from>jack@writeit.com</from>
   <to>john@xmli.com</to>
   <body>memo body goes here</body>
</envelope>




清单 3. 数字资产词汇表
				
<photo>
   <subject>Westlicht Museum of Camera and Photography, Vienna</subject>
   <date>April 25, 2005</date>
   <description>Lobby of the museum</description>
   <camera>Nikon D70</camera>
   <frame>5643</frame>
</photo>

数字资产如果通过消息平台发送就会出现冲突,因为软件混淆了两个词汇表中的 subject 和 date 标签。换句话说,标签名不是全局标识符。

XML 名称空间通过在标签名前增加全局标识符将本地名转化成全局名。为了保证全局标识符的唯一性,全局标识符必须是 URI(就是说很可能包含注册的域名以保证唯一性)。结果如清单 4 所示。


清单 4. 组合词汇表
				
<env:envelope xmlns:env="http://psol.com/2005/env"
              xmlns:ph="http://psol.com/2005/photo">
   <env:subject>Latest photo</env:subject>
   <env:date>April 27, 2005</env:date>
   <env:from>jack@writeit.com</env:from>
   <env:to>john@xmli.com</env:to>
   <env:body>
      <ph:photo>
         <ph:subject>Westlicht Museum
             of Camera and Photography, Vienna</ph:subject>
         <ph:date>April 25, 2005</ph:date>
         <ph:description>Lobby of the museum</ph:description>
         <ph:camera>Nikon D70</ph:camera>
         <ph:frame>5643</ph:frame>
     </ph:photo></env:body>
</env:envelope>

我来澄清常常被误解的两件事:

  • URI 是标识符,不是地址。
  • 前缀不是标识符。

URI 和地址

虽然一般的 URI 是地址,但对于 XML 名称空间来说,它们仅作为标识符来使用。。我希望名称空间像 Java 包那样来标识,但是不能这样做——比如用 com.psol.vocabulary 代替更容易造成混淆的 http://psol.com/vocabulary

既然是标识符,这个地址就可能是无效的,就是说如果尝试打开该地址就会返回“404 - Resource not found”错误。但它们仍然用于这个目的。和一般的想法相反,名称空间 URI 并不指向 W3C XML Schema。

其次,因为这个上下文中 URI 是标识符,应用程序必须逐字符地匹配该 URI。修改 XML 词汇表的 URI,比方说让它指向您的服务器,这种做法是错误的。比如,XSL 的 URI 是 http://www.w3.org/1999/XSL/Transform。如果您在BM®工作,也不能将其改为 http://www.ibm.com/1999/XSL/Transform。事实上,根本不能改变已有词汇表的 URI。

我在讲授 XSLT 时,学生常常抱怨处理程序不工作,而实际上是他们没有正确的复制 XSLT URI。

结论之一是,应该避免修改名称空间。在 URI 中包含版本模式通常不是一个好主意,这样注定会让应用程序无法工作(不错,我承认 W3C 对 SOAP 是这样做的)。

前缀

另一种常见的错误是混淆了前缀和标识符。标签名不是标识符,同样,前缀也不是标识符。两个不同应用程序使用同一前缀的风险很大。因此,名称空间前缀是透明的,不应该在应用程序中显式地操纵前缀。但是,XML 作者在文档中修改前缀是完全合理的(比如为了避免冲突)。

因此应该避免清单 5 这样的代码, 而仿效清单 6 中的写法。


清单 5. 不正确的前缀测试
				
startElement(String uri,String local,String qname,Attributes atts) 
{
   if(qname.equals("env:Envelope"))
      ;   // do something
}




清单 6. 正确的测试名称空间 URI
				
startElement(String uri,String local,String qname,Attributes atts) 
{
   if(uri.equals("http://psol.com/2005/envelope")
      && local.equals("Envelope"))
      ;   // do something
}





回页首


结束语

只要记住这些陷阱,就可以在很大程度上改进您的 XML 编码。更重要的是,这样可以降低不兼容的危险,极大地简化 XML 应用程序的维护。本系列的后续文章将考察和 XML 应用而非语法有关的常见陷阱。



参考资料



关于作者

Benoit Marchal 的照片

BenoÎt Marchal 是一位比利时籍顾问。他是 XML by Example, Second Edition 和其他几本 XML 书籍的作者。您可以通过 bmarchal@pineapplesoft.com 或者他的个人站点 www.marchal.com 和他联系。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

将您的建议发给我们或者通过参加讨论与其他人分享您的想法.







回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款