在跨系统数据交换中,保证XML报文完整性与来源可信是常见需求。XML签名规范(XML Signature Syntax and Processing)定义了一套标准结构,让签名信息本身也以XML形式存在,便于在不同平台间传递与校验。Java从标准库到第三方库都提供了实现能力,理解其工作流程比直接复制代码更重要。

一、XML签名的基本概念
XML签名并不是简单对字符串做哈希再加密,而是先将待签数据经过规范化(Canonicalization)处理,消除空格、属性顺序、命名空间声明等差异,再计算摘要并加密生成签名值。最终签名结果放在Signature元素中,可嵌入原文档,也可独立成文件。
根据签名覆盖范围,常见有三种类型:enveloped 签名将 Signature 节点放在被签文档内部;enveloping 签名把被签数据嵌在 Signature 内;detached 签名则签名外部资源,Signature 仅通过 URI 引用。实际开发中 enveloped 最普遍,因为它不破坏原有报文结构。
1.1 核心元素说明
SignedInfo是签名的元数据容器,包含规范化算法、签名算法以及至少一个Reference。每个 Reference 通过 URI 指向目标,并声明摘要算法与转换链。SignatureValue存放 base64 编码的签名结果,KeyInfo可选,用于携带公钥或证书。
如果忽略规范化配置,Java 默认可能采用包容性算法,导致不同解析器序列化出的字节不同,验签必然失败。因此明确指定exc_c14n等排他性规范化方法,是跨语言互通的关键。
二、使用Java标准API生成enveloped签名
JDK自带 javax.xml.crypto.dsig 包,无需额外依赖即可完成签名。下面示例用RSA私钥对已有XML文档做 enveloped 签名,并写入 Signature 节点。
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.*;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
public class XmlSignDemo {
public static void main(String[] args) throws Exception {
// 加载PKCS12密钥库
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream("keystore.p12"), "password".toCharArray());
PrivateKey privateKey = (PrivateKey) ks.getKey("alias", "password".toCharArray());
X509Certificate cert = (X509Certificate) ks.getCertificate("alias");
// 解析待签文档
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document doc = dbf.newDocumentBuilder().parse("input.xml");
// 创建签名工厂
XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");
DigestMethod digestMethod = factory.newDigestMethod(DigestMethod.SHA256, null);
Transform enveloped = factory.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
Transform c14n = factory.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", (C14NMethodParameterSpec) null);
Reference ref = factory.newReference("", digestMethod, java.util.Arrays.asList(enveloped, c14n), null, null);
SignatureMethod signMethod = factory.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null);
CanonicalizationMethod canonMethod = factory.newCanonicalizationMethod("http://www.w3.org/2001/10/xml-exc-c14n#", (C14NMethodParameterSpec) null);
SignedInfo signedInfo = factory.newSignedInfo(canonMethod, signMethod, java.util.Collections.singletonList(ref));
// 构造KeyInfo
KeyInfoFactory kif = factory.getKeyInfoFactory();
KeyInfo keyInfo = kif.newKeyInfo(java.util.Collections.singletonList(kif.newX509Data(java.util.Collections.singletonList(cert))));
XMLSignature signature = factory.newXMLSignature(signedInfo, keyInfo);
DOMSignContext signContext = new DOMSignContext(privateKey, doc.getDocumentElement());
signature.sign(signContext);
// 输出结果
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult("signed.xml"));
}
}
上述代码先准备私钥与证书,再从文件读取需签名的XML。注意 Reference 的 URI 设为空字符串,表示签名根元素下的全部内容(排除 Signature 自身)。两个 Transform 依次执行 enveloped 排除签名节点、exc_c14n 规范化,避免命名空间前缀变动引发验签错误。
运行后生成的 signed.xml 在原根节点内多了 Signature 子元素。若业务系统用不同XML库重新序列化,只要规范化算法一致,摘要就不会变。这也是为什么不能直接对字符串哈希,而要走标准转换链。
2.1 常见错误与排查
新手常把 DocumentBuilderFactory 的命名空间感知关掉,导致签名上下文找不到正确命名空间,抛出 ClassCastException 或验签无匹配节点。务必设置 setNamespaceAware(true)。
另一个坑是密钥库类型。Java 8 后推荐 PKCS12 而非 JKS,若用错类型加载会报密钥格式异常。证书链不完整时,验签方也可能拒绝信任,需在 KeyInfo 或外部配置可信锚。
三、使用Apache Santuario验证签名
验证可继续用标准API,也可引入 Apache Santuario 获得更友好校验控制。以下展示标准方式验签已生成的 enveloped 签名。
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import javax.xml.crypto.dsig.XMLSignatureFactory;
public class XmlVerifyDemo {
public static void main(String[] args) throws Exception {
KeyStore ts = KeyStore.getInstance("PKCS12");
ts.load(new FileInputStream("truststore.p12"), "password".toCharArray());
X509Certificate trustedCert = (X509Certificate) ts.getCertificate("alias");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document doc = dbf.newDocumentBuilder().parse("signed.xml");
NodeList nl = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature");
XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");
DOMValidateContext context = new DOMValidateContext(trustedCert.getPublicKey(), nl.item(0));
XMLSignature signature = factory.unmarshalXMLSignature(context);
boolean valid = signature.validate(context);
// 若需细查引用校验
if (!valid) {
signature.getSignedInfo().getReferences().forEach(ref -> {
try {
boolean refValid = ((javax.xml.crypto.dsig.Reference) ref).validate(context);
System.out.println("Reference valid: " + refValid);
} catch (Exception e) {
e.printStackTrace();
}
});
}
System.out.println("Signature valid: " + valid);
}
}
验证时把可信公钥或证书公钥放入 DOMValidateContext。若签名内 KeyInfo 含证书,也可从文档提取再比对信任库,但显式传入公钥更简单且防伪造。代码遍历 Signature 节点,调用 validate 即完成整体检查。
当返回 false,不要只打印失败,应逐个 Reference 调用 validate,因为可能是摘要不匹配(内容被改)或签名值不对(密钥错)。上面的循环演示了如何定位是哪一层出错,方便联调。
3.1 detached签名验证差异
如果签名是 detached 形态,Reference 的 URI 指向外部文件或文档内某个带Id的属性节点。验证方必须能解析该 URI,否则报找不到引用。此时要确保被签资源在验签环境可达,且 Id 属性声明为 xml:id 或相应命名空间,否则选择器无效。
相比 enveloped,detached 更适合签名大文件或已存在日志,不改变原文档。但配置复杂度高,跨系统时要约定 URI 解析规则,避免一方用相对路径另一方用绝对路径。
四、选型与性能建议
标准API零依赖,适合内网或可控环境;Apache Santuario 提供批处理、流式处理及更全算法,适合网关类高并发场景。签名本身耗时在规范化与RSA运算,SHA256配合2048位密钥在普通服务器单次约几毫秒,批量可缓存 Document 解析结果。
务必把规范化算法写死为 exc_c14n,不要依赖默认。证书有效期与吊销检查也属于验签链路,生产环境应接 OCSP 或CRL,而非仅比公钥。这样 Java 生成的 XML 签名才能稳定通过多方校验。
XML_signatureJava_securityApache_Santuario修改时间:2026-08-07 20:15:40