Java如何生成和验证XML签名?

来源:网络编程作者:鱼儿头衔:草根站长
导读:本期聚焦于小伙伴创作的《Java如何生成和验证XML签名?》,敬请观看详情。数字接口对接时常要求对XML报文做防篡改处理,但不少团队把签名值直接拼在业务字段里,结果验签时命名空间一变就失败。XML签名有一套独立的规范,它把待签内容、算法和证书信息封装进Signature节点,支持对整体文档、局部元素或仅内容做签名。Java生态里可通过标准API或Apache Santuario实现,核心在于正确选取转换规则与规范化方法。本文说明用Java构造与校验XML签名的完整步骤,并比较 enveloped 与 detached 两种常用形态的差异,帮你避开规范化和命名空间引起的验签异常。

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

Java如何生成和验证XML签名?

一、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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。