當需要建立數位簽章並傳送給同儕進行驗證時,通常會使用 XML 正規化。由於數位簽章是根據 XML 資料建立的,因此 XML 資料必須先經過正規化,才能計算其簽章值。即使是一個額外的空格也可能會影響計算出的簽章值,因此必須遵循一些規則來正規化 XML 資料,使其具有標準格式。這就是為什麼 W3C 制定了 Canonical XML Version 1.1 規範的原因。
此規範提供了格式化元素節點、屬性節點和命名空間節點等的規則。不同的程式語言都實作了此規範,因此我們可以輕鬆地使用它們來正規化 XML 資料,而無需了解規範的詳細資訊。
在本教學中,我們不會深入探討規範的細節,我們只會專注於如何呼叫 Java API 來正規化 XML 資料。根據 XPath 資料模型,XML 文件是由一組節點(節點集)表示的。而 Java 中的 XML API 接收一個要正規化的 Data 物件。為了示範正規化,我們將建立一個自訂的 NodeSetDataImpl,它實作了 NodeSetData 和 Iterator 介面。
import java.util.Iterator; import javax.xml.crypto.NodeSetData; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.traversal.DocumentTraversal; import org.w3c.dom.traversal.NodeFilter; import org.w3c.dom.traversal.NodeIterator; public class NodeSetDataImpl implements NodeSetData, Iterator { private Node ivNode; private NodeFilter ivNodeFilter; private Document ivDocument; private DocumentTraversal ivDocumentTraversal; private NodeIterator ivNodeIterator; private Node ivNextNode; public NodeSetDataImpl(Node pNode, NodeFilter pNodeFilter) throws Exception { ivNode = pNode; ivNodeFilter = pNodeFilter; if (ivNode instanceof Document) { ivDocument = (Document) ivNode; } else { ivDocument = ivNode.getOwnerDocument(); } ivDocumentTraversal = (DocumentTraversal) ivDocument; } private NodeSetDataImpl(NodeIterator pNodeIterator) { ivNodeIterator = pNodeIterator; } public Iterator iterator() { NodeIterator nodeIterator = ivDocumentTraversal.createNodeIterator(ivNode, NodeFilter.SHOW_ALL, ivNodeFilter, false); return new NodeSetDataImpl(nodeIterator); } private Node checkNextNode() { if (ivNextNode == null && ivNodeIterator != null) { ivNextNode = ivNodeIterator.nextNode(); if (ivNextNode == null) { ivNodeIterator.detach(); ivNodeIterator = null; } } return ivNextNode; } private Node consumeNextNode() { Node nextNode = checkNextNode(); ivNextNode = null; return nextNode; } public boolean hasNext() { return checkNextNode() != null; } public Node next() { return consumeNextNode(); } public void remove() { throw new UnsupportedOperationException("Removing nodes is not supported."); } public static NodeFilter getRootNodeFilter() { return new NodeFilter() { public short acceptNode(Node pNode) { if (pNode instanceof Element && pNode.getParentNode() instanceof Document) { return NodeFilter.FILTER_SKIP; } return NodeFilter.FILTER_ACCEPT; } }; } }
然後我們需要建立一個 CanonicalizationMethod 來執行實際的正規化。這裡有一些選擇:
CanonicalizationMethod.INCLUSIVE CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS CanonicalizationMethod.EXCLUSIVE CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS
在本教學中,我們只會示範包含式正規化。以下是執行此操作的程式碼。
import java.io.ByteArrayInputStream; import javax.xml.crypto.Data; import javax.xml.crypto.OctetStreamData; import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; public class CanonicalizationTest { private static String INPUT = "" + "hello" + "world" + ""; public static void main(String[] args){ transform(INPUT); } /** * Create new document with data * * @param xml * @return */ private static Document createNewDocument(String xml){ try{ byte[] bytes = xml.getBytes("UTF-8"); ByteArrayInputStream bin = new ByteArrayInputStream(bytes); DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance(); fac.setNamespaceAware(true); DocumentBuilder docBuilder = fac.newDocumentBuilder(); Document doc = docBuilder.parse(bin); return doc; } catch (Exception ex){ ex.printStackTrace(); } return null; } /** * Transform a XML string * * @param xml */ private static void transform(String xml){ Document doc = createNewDocument(xml); try{ Data data = new NodeSetDataImpl(doc, NodeSetDataImpl.getRootNodeFilter()); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); CanonicalizationMethod canonicalizationMethod = fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec)null); // Doing the actual canonicalization OctetStreamData transformedData =(OctetStreamData) canonicalizationMethod.transform(data, null); byte[] bytes = tool.Util.readStream(transformedData.getOctetStream()); String str = new String(bytes); System.out.println(str); } catch (Exception ex){ ex.printStackTrace(); } } }
首先,讓我們看一下輸出:
<my:Node xmlns:my="http://example.com" Id="1">hello</my:Node><my:Node xmlns:my="http://example.com" Id="2">world</my:Node>
測試會先建立一個 Document,然後將此 Document 傳遞給 NodeSetDataImpl,以建立要正規化的節點集。從輸出中,我們可以看到 Root 節點在正規化的資料中被移除,這是因為 NodeSetDataImpl 中的 NodeFilter 過濾了此節點。接下來,第二個 my:Node 在正規化形式中,xmlns:my 節點位於 Id 節點之前。這是基於 Canonical XML 規範,其中節點應按詞彙順序排列。
對於其他正規化方法,這裡的邏輯是相同的。不同之處在於輸出不相同,因為不同的正規化方法有不同的規則。