You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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-c14n Transform,这样验证方会自动处理属性顺序差异。
  • 只有当对方的验证逻辑未正确应用规范化Transform时,才需要强制属性顺序一致,此时方案二或三更合适。

内容的提问来源于stack exchange,提问作者pmpreciado

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:49:41