XML属性顺序变更致Java SOAP服务数字签名失效求助
解决Java SOAP Web服务签名中属性顺序导致的摘要不匹配问题
首先得明确:XML属性顺序本身不影响语义,但你遇到的核心矛盾是签名环节DOM节点的属性顺序与JAX-WS序列化输出的属性顺序不一致,导致直接计算原始XML的摘要值不匹配。不过严格来说,只要签名时配置了正确的exc-c14n(Exclusive XML Canonicalization)规范化算法,验证方应该会对收到的节点应用同样的规范化处理,从而得到一致的摘要。如果你的场景要求发送的XML与签名时的节点结构完全一致(包括属性顺序),可以尝试以下几种方案:
方案一:强制对SOAP响应应用exc-c14n规范化
既然签名时已经使用http://www.w3.org/2001/10/xml-exc-c14n#作为规范化算法,直接对整个出站SOAP消息应用该规范,就能让发送的XML属性顺序和签名计算时完全一致。可以通过SOAPHandler实现:
public class CanonicalizationHandler implements SOAPHandler<SOAPMessageContext> { private static final String EXC_C14N_URI = "http://www.w3.org/2001/10/xml-exc-c14n#"; @Override public boolean handleMessage(SOAPMessageContext context) { Boolean isOutbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); if (isOutbound) { try { SOAPMessage soapMsg = context.getMessage(); // 将SOAP消息转换为DOM Document Document doc = soapMsg.getSOAPPart().getEnvelope().getOwnerDocument(); // 创建exc-c14n规范化实例 XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM"); CanonicalizationMethod c14nMethod = sigFactory.newCanonicalizationMethod( EXC_C14N_URI, (C14NMethodParameterSpec) null); // 对整个文档应用规范化 byte[] canonicalBytes = c14nMethod.canonicalizeSubtree(doc); // 用规范化后的内容重新创建SOAP消息 MessageFactory msgFactory = MessageFactory.newInstance(); SOAPMessage newSoapMsg = msgFactory.createMessage(); newSoapMsg.getSOAPPart().setContent(new StreamSource(new ByteArrayInputStream(canonicalBytes))); newSoapMsg.saveChanges(); // 替换原消息 context.setMessage(newSoapMsg); } catch (Exception e) { e.printStackTrace(); return false; } } return true; } @Override public Set<QName> getHeaders() { return Collections.emptySet(); } @Override public boolean handleFault(SOAPMessageContext context) { return true; } @Override public void close(MessageContext context) { // 清理资源 } }
将这个Handler注册到你的JAX-WS端点,出站响应就会先经过exc-c14n规范化,属性顺序会严格遵循规范,与签名计算时的结构完全一致。
方案二:控制JAXB序列化的属性顺序
如果你的SOAP响应是通过JAXB生成的,可以通过自定义StAX序列化逻辑,强制属性顺序(先写wsu:Id,再写命名空间声明):
public class AttributeOrderHandler implements SOAPHandler<SOAPMessageContext> { @Override public boolean handleMessage(SOAPMessageContext context) { Boolean isOutbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); if (isOutbound) { try { SOAPMessage soapMsg = context.getMessage(); Document doc = soapMsg.getSOAPPart().getEnvelope().getOwnerDocument(); // 使用StAX Writer手动控制属性顺序 StringWriter sw = new StringWriter(); XMLStreamWriter writer = XMLOutputFactory.newInstance().createXMLStreamWriter(sw); traverseAndWrite(doc.getDocumentElement(), writer); writer.close(); // 重新创建SOAP消息 MessageFactory msgFactory = MessageFactory.newInstance(); SOAPMessage newSoapMsg = msgFactory.createMessage(); newSoapMsg.getSOAPPart().setContent(new StreamSource(new StringReader(sw.toString()))); newSoapMsg.saveChanges(); context.setMessage(newSoapMsg); } catch (Exception e) { e.printStackTrace(); return false; } } return true; } private void traverseAndWrite(Node node, XMLStreamWriter writer) throws XMLStreamException { if (node instanceof Element) { Element elem = (Element) node; writer.writeStartElement(elem.getPrefix(), elem.getLocalName(), elem.getNamespaceURI()); // 先写入非命名空间属性(比如wsu:Id) NamedNodeMap attrs = elem.getAttributes(); for (int i = 0; i < attrs.getLength(); i++) { Attr attr = (Attr) attrs.item(i); if (!attr.getName().startsWith("xmlns")) { writer.writeAttribute(attr.getPrefix(), attr.getNamespaceURI(), attr.getLocalName(), attr.getValue()); } } // 再写入命名空间声明 for (int i = 0; i < attrs.getLength(); i++) { Attr attr = (Attr) attrs.item(i); if (attr.getName().startsWith("xmlns")) { if (attr.getPrefix().isEmpty()) { writer.writeDefaultNamespace(attr.getValue()); } else { writer.writeNamespace(attr.getLocalName(), attr.getValue()); } } } // 复制子节点 NodeList children = elem.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { traverseAndWrite(children.item(i), writer); } writer.writeEndElement(); } else if (node instanceof Text) { writer.writeCharacters(((Text) node).getData().trim()); } } // 实现其他Handler接口方法 @Override public Set<QName> getHeaders() { return Collections.emptySet(); } @Override public boolean handleFault(SOAPMessageContext context) { return true; } @Override public void close(MessageContext context) {} }
这种方法通过手动遍历DOM节点并控制写入顺序,确保wsu:Id属性在命名空间声明之前输出,与JAX-WS默认的序列化顺序一致。
方案三:修改签名环节的DOM节点属性顺序
在签名前手动调整Body和Timestamp节点的属性顺序,让它们的属性顺序与JAX-WS输出一致:
// 调整Timestamp节点的属性顺序示例 private static SOAPElement reorderAttributes(SOAPElement originalElem, String nsUri, String idAttrName) throws SOAPException { SOAPElement newElem = originalElem.getParentElement().addChildElement( originalElem.getLocalName(), originalElem.getPrefix(), originalElem.getNamespaceURI()); // 先添加wsu:Id属性 String idValue = originalElem.getAttributeValue(new QName(nsUri, idAttrName, "wsu")); newElem.addAttribute(new QName(nsUri, idAttrName, "wsu"), idValue); // 再添加命名空间声明 newElem.addNamespaceDeclaration("wsu", nsUri); // 复制所有子节点 NodeList children = originalElem.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { newElem.appendChild(newElem.getOwnerDocument().importNode(children.item(i), true)); } // 替换原始节点 originalElem.getParentElement().replaceChild(newElem, originalElem); return newElem; } // 在crearFirmaSeparada方法调用前使用 nodoTimestamp = reorderAttributes(nodoTimestamp, NAMESPACE_WSU, "Id"); soapBody = (SOAPBody) reorderAttributes((SOAPElement) soapBody, NAMESPACE_WSU, "Id");
调整后的节点属性顺序与JAX-WS输出一致,签名时计算的摘要自然会匹配。
关键注意事项
- 优先推荐方案一:它严格遵循XML签名规范,通过规范化算法统一结构,即使属性顺序不同,验证方也能得到一致的摘要,是最健壮的解决方案。
- 确保签名代码中的
Reference配置正确:必须包含exc-c14nTransform,这样验证方会自动处理属性顺序差异。 - 只有当对方的验证逻辑未正确应用规范化Transform时,才需要强制属性顺序一致,此时方案二或三更合适。
内容的提问来源于stack exchange,提问作者pmpreciado
相关产品推荐
相关产品推荐

