Apache Santuario 构建并运行示例应用指南

Apache Santuario 3.0.6 构建并运行示例应用指南:从零创建 Maven 工程、完整 pom.xml、RSA-SHA256 enveloped 签名/验签/篡改验签失败三段示例源码、运行命令与真实回显(signature valid: true、篡改后 false)、jar 直跑方案与 JVM 堆限制验证,全程实测。

Apache SantuarioApache Santuario2026/10/12

Apache Santuario 构建并运行示例应用指南

目标:从零构建并运行一个基于 Apache Santuario 3.0.6(制品名 xmlsec)的 XML 签名最小应用,覆盖完整闭环:enveloped 签名 → 验签成功(signature valid: true)→ 篡改金额后验签失败(signature valid: false)。给出完整 pom.xml、示例数据、三段示例源码、编译运行命令与真实回显,并附 jar 直跑方案。所有命令与输出均在 Ubuntu 22.04.5 + OpenJDK 17 环境实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS
  • Java:OpenJDK 17.0.20.1
  • 构建工具:Apache Maven 3.6.3
  • Santuario 版本:xmlsec 3.0.6(3.x 线最新稳定版,Maven Central 实查;4.0.4 属 4.x 新主线)
  • 示例工程目录:/root/xmlsec-demo/
  • 演示账号:root

关键版本回显:

$ java -version
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "6.8.0-138-generic", arch: "amd64", family: "unix"

二、前置条件

  • 已按《Apache Santuario 安装指南》完成 JDK 17 与 Maven 安装,java -version、mvn -version 可用。

  • 已安装 curl、wget(方案 B 需要):

    apt-get install -y -qq curl wget
    

    回显收尾:

    Processing triggers for libc-bin (2.35-0ubuntu3.15) ...
    
  • xmlsec 依赖将以 Maven 方式引入(坐标 org.apache.santuario:xmlsec:3.0.6),Maven Central 直连可用。

三、工程结构

标准 Maven 布局,1 个 pom、3 个示例类、1 份待签名数据:

xmlsec-demo/
├── pom.xml
├── invoice.xml
└── src/main/java/org/example/
    ├── SignExample.java
    ├── ValidateExample.java
    └── ValidateTamperedExample.java

创建目录:

mkdir -p /root/xmlsec-demo/src/main/java/org/example

三个类的职责:

类职责
SignExample生成 RSA 2048 密钥对,对 invoice.xml 做 enveloped 签名,输出 signed.xml
ValidateExample读取公钥对 signed.xml 验签(开启 secure validation)
ValidateTamperedExample把金额 12800.00 篡改为 99999.00 后验签,演示失败场景

四、完整 pom.xml

写入 /root/xmlsec-demo/pom.xml。exec-maven-plugin 用于以 mvn exec:java 直接运行主类:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>xmlsec-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.santuario</groupId>
            <artifactId>xmlsec</artifactId>
            <version>3.0.6</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.1.0</version>
            </plugin>
        </plugins>
    </build>
</project>

首次编译验证(会从 Maven Central 下载依赖):

$ cd /root/xmlsec-demo && mvn clean compile
...
[INFO] Compiling 3 source files to /root/xmlsec-demo/target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:25 min
[INFO] Finished at: 2026-10-12T02:42:14Z
[INFO] ------------------------------------------------------------------------

五、示例数据与源码

5.1 待签名数据 invoice.xml

在工程根目录 /root/xmlsec-demo/ 创建 invoice.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Invoice id="INV-2026-001">
  <Customer>Acme Corp</Customer>
  <Amount currency="CNY">12800.00</Amount>
  <Items>
    <Item id="A-1001">
      <Name>XML Security Handbook</Name>
      <Quantity>2</Quantity>
    </Item>
    <Item id="A-1002">
      <Name>Applied Cryptography</Name>
      <Quantity>1</Quantity>
    </Item>
  </Items>
</Invoice>

5.2 签名类 SignExample.java

创建 /root/xmlsec-demo/src/main/java/org/example/SignExample.java。要点:显式传入 Santuario 提供者 XMLDSigRI;对整个文档(URI="")做 enveloped 转换 + SHA256 摘要;RSA-SHA256 签名;密钥对落盘为 PEM:

package org.example;

import org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI;

import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.Base64;
import java.util.Collections;

public class SignExample {
    public static void main(String[] args) throws Exception {
        // 1) 生成 RSA 2048 密钥对并落盘(PKCS#8 私钥 / X.509 公钥,PEM 编码)
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(2048);
        KeyPair kp = kpg.generateKeyPair();
        Base64.Encoder b64 = Base64.getMimeEncoder(64, "\n".getBytes());
        Files.write(Paths.get("private.key"),
                ("-----BEGIN PRIVATE KEY-----\n" + b64.encodeToString(kp.getPrivate().getEncoded())
                        + "\n-----END PRIVATE KEY-----\n").getBytes());
        Files.write(Paths.get("public.key"),
                ("-----BEGIN PUBLIC KEY-----\n" + b64.encodeToString(kp.getPublic().getEncoded())
                        + "\n-----END PUBLIC KEY-----\n").getBytes());
        System.out.println("[Info] RSA 2048 keypair generated: private.key / public.key");

        // 2) 解析待签名 XML(namespace aware)
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        org.w3c.dom.Document doc = dbf.newDocumentBuilder().parse(new File("invoice.xml"));

        // 3) 显式注册 Apache Santuario 的 JSR-105 提供者 XMLDSigRI
        XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM", new XMLDSigRI());
        System.out.println("[Info] XMLSignatureFactory provider: " + fac.getProvider().getName()
                + " - " + fac.getProvider().getInfo());

        // 4) Reference:对整个文档(URI="")做 enveloped 转换 + SHA256 摘要
        DigestMethod dm = fac.newDigestMethod(DigestMethod.SHA256, null);
        Transform tf = fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
        Reference ref = fac.newReference("", dm, Collections.singletonList(tf), null, null);

        // 5) SignedInfo:inclusive c14n + RSA-SHA256 签名算法
        SignedInfo si = fac.newSignedInfo(
                fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
                fac.newSignatureMethod(SignatureMethod.RSA_SHA256, null),
                Collections.singletonList(ref));

        // 6) KeyInfo:携带 RSA 公钥(KeyValue)
        KeyInfoFactory kif = fac.getKeyInfoFactory();
        KeyValue kv = kif.newKeyValue(kp.getPublic());
        KeyInfo ki = kif.newKeyInfo(Collections.singletonList(kv));

        // 7) 构造签名对象并签入文档(签名作为根元素最后一个子节点)
        XMLSignature signature = fac.newXMLSignature(si, ki);
        DOMSignContext dsc = new DOMSignContext(kp.getPrivate(), doc.getDocumentElement());
        signature.sign(dsc);
        System.out.println("[Info] document signed, Signature appended to root element");

        // 8) 序列化到 signed.xml(不启用 indent,保证输出与被签内容字节级一致)
        Transformer t = TransformerFactory.newInstance().newTransformer();
        t.transform(new DOMSource(doc), new StreamResult(new FileOutputStream("signed.xml")));
        System.out.println("[Info] signed document written to signed.xml");
    }
}

5.3 验签类 ValidateExample.java

创建 /root/xmlsec-demo/src/main/java/org/example/ValidateExample.java。要点:开启 secure validation;核心验证通过后逐 Reference 输出有效性:

package org.example;

import org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI;

import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Iterator;

public class ValidateExample {
    public static void main(String[] args) throws Exception {
        // 1) 读取公钥(PEM -> DER -> PublicKey)
        String pem = new String(Files.readAllBytes(Paths.get("public.key")))
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");
        PublicKey publicKey = KeyFactory.getInstance("RSA")
                .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(pem)));

        // 2) 解析待验证文档并定位 Signature 节点
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        org.w3c.dom.Document doc = dbf.newDocumentBuilder().parse(new File("signed.xml"));
        XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM", new XMLDSigRI());
        org.w3c.dom.NodeList sigs = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
        if (sigs.getLength() == 0) {
            throw new IllegalStateException("no Signature element found");
        }
        DOMValidateContext vc = new DOMValidateContext(publicKey, sigs.item(0));

        // 3) 开启安全校验(secure validation)
        vc.setProperty("org.apache.jcp.xml.dsig.secureValidation", Boolean.TRUE);

        // 4) unmarshal + 验签
        XMLSignature signature = fac.unmarshalXMLSignature(vc);
        boolean coreValid = signature.validate(vc);
        System.out.println("signature valid: " + coreValid);
        if (coreValid) {
            Iterator<?> it = signature.getSignedInfo().getReferences().iterator();
            for (int j = 0; it.hasNext(); j++) {
                boolean refValid = ((Reference) it.next()).validate(vc);
                System.out.println("ref[" + j + "] validity status: " + refValid);
            }
        }
    }
}

5.4 篡改验签类 ValidateTamperedExample.java

创建 /root/xmlsec-demo/src/main/java/org/example/ValidateTamperedExample.java。要点:解析 signed.xml 后把 <Amount> 文本改为 99999.00,另存 tampered.xml,再对篡改后的文档验签:

package org.example;

import org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI;

import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class ValidateTamperedExample {
    public static void main(String[] args) throws Exception {
        // 1) 读取公钥(PEM -> DER -> PublicKey)
        String pem = new String(Files.readAllBytes(Paths.get("public.key")))
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");
        PublicKey publicKey = KeyFactory.getInstance("RSA")
                .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(pem)));

        // 2) 解析 signed.xml 并篡改 <Amount> 金额
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        org.w3c.dom.Document doc = dbf.newDocumentBuilder().parse(new File("signed.xml"));
        org.w3c.dom.Node amount = doc.getElementsByTagName("Amount").item(0);
        System.out.println("[Info] Amount before tamper: " + amount.getTextContent());
        amount.setTextContent("99999.00");
        System.out.println("[Info] Amount after tamper: 99999.00");

        // 3) 篡改后的文档另存 tampered.xml
        Transformer t = TransformerFactory.newInstance().newTransformer();
        t.transform(new DOMSource(doc), new StreamResult(new FileOutputStream("tampered.xml")));

        // 4) 对篡改后的文档验签
        XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM", new XMLDSigRI());
        org.w3c.dom.NodeList sigs = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
        DOMValidateContext vc = new DOMValidateContext(publicKey, sigs.item(0));
        vc.setProperty("org.apache.jcp.xml.dsig.secureValidation", Boolean.TRUE);
        XMLSignature signature = fac.unmarshalXMLSignature(vc);
        boolean coreValid = signature.validate(vc);
        System.out.println("signature valid: " + coreValid);
    }
}

六、编译与运行

先编译,再依次运行三个主类。运行前把示例进程堆内存限制在 256MB–512MB:

cd /root/xmlsec-demo
mvn -q clean compile
export MAVEN_OPTS="-Xms256m -Xmx512m"

6.1 步骤一:签名(SignExample)

$ cd /root/xmlsec-demo && mvn -q compile exec:java -Dexec.mainClass=org.example.SignExample
[Info] RSA 2048 keypair generated: private.key / public.key
[Info] XMLSignatureFactory provider: ApacheXMLDSig - Apache Santuario XMLDSig (DOM XMLSignatureFactory; DOM KeyInfoFactory; C14N 1.0, C14N 1.1, Exclusive C14N, Base64, Enveloped, XPath, XPath2, XSLT TransformServices)
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[Info] document signed, Signature appended to root element
[Info] signed document written to signed.xml

运行后在工程目录生成 private.key、public.key、signed.xml。注意:每次运行 SignExample 都会重新生成密钥对并覆盖这两个密钥文件,如需复用密钥请先备份。

6.2 步骤二:验签(ValidateExample)

$ mvn -q compile exec:java -Dexec.mainClass=org.example.ValidateExample
signature valid: true
ref[0] validity status: true

6.3 步骤三:篡改后验签(ValidateTamperedExample)

$ mvn -q compile exec:java -Dexec.mainClass=org.example.ValidateTamperedExample
[Info] Amount before tamper: 12800.00
[Info] Amount after tamper: 99999.00
signature valid: false

七、运行结果解读

7.1 签名产物 signed.xml

cat signed.xml 实测全文(Signature 已追加到根元素 Invoice 末尾):

$ cat signed.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?><Invoice id="INV-2026-001">
  <Customer>Acme Corp</Customer>
  <Amount currency="CNY">12800.00</Amount>
  <Items>
    <Item id="A-1001">
      <Name>XML Security Handbook</Name>
      <Quantity>2</Quantity>
    </Item>
    <Item id="A-1002">
      <Name>Applied Cryptography</Name>
      <Quantity>1</Quantity>
    </Item>
  </Items>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>i5VbidKnNm0Vwc9nYeLa7sfwXRJZmaIHCD2QU/nKblA=</DigestValue></Reference></SignedInfo><SignatureValue>aRTStLdQ9TXnxUqbQfPPFAg0S6IJ3KJa6p9s8bV2vDFDOxCZBpxWIsXWXyNG56s05SpqSvZrIJDg&#13;
DgaTfTY9HxLtzWZnaIoJ4gFTq3USiWrY6Lti+NmJl/fpItZjBAe2a2YI8Xg6ikcnvEkwU5HScAdg&#13;
yf9ipNYywIAwowVfvUjmCUTazIU5Y1Mm8oFrRGmnV/MLtlUiVpsHw6NK21/J4QEp0gdmdde82fHQ&#13;
jhSzeIsACq31XE3h7tqvB5KfadKzPRUddOLXsHjJMqvpaEvelAHXIh/J+LJM18volojzJeRNuBTH&#13;
3QLS5KjmsDwrjZ0/O0Zs+cUYB5Sfp3/Fv8Nj4w==</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>xObYGfy7IQvO3IWFxLDoGLoaUlhTCD+ZfN3WAhIg3bXAnoj0FZvSgQMeEDL9z2ul70K2oa9+bDiM&#13;
ggRka7dN0UIkWnr0MXRqLLVEMzwy24sk/8yMFtTiLpFEyOM/sg4epI5E/Xq/sig9EhR1cl8yumYv&#13;
L7w0IkhaGr3jFKS5G0XOPJPuULqGi44QwHPDERov74jryUHCVVUULX5XfWgMJXHvGUbHFJPFXPse&#13;
WB4Xumqxq02uO+197g9YzsqcXY8OoKvnK2T0IWOo00GAa+uUz7mpB5n5EIJiOrgC15zi2LZzDcS5&#13;
+qo1jzBYTpkwRGVOZdqe5sC0SKgHBldrUWZexQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue></KeyInfo></Signature></Invoice>

结构要点:

  • CanonicalizationMethod:inclusive c14n(http://www.w3.org/TR/2001/REC-xml-c14n-20010315),与代码中 CanonicalizationMethod.INCLUSIVE 对应。
  • SignatureMethod:rsa-sha256(http://www.w3.org/2001/04/xmldsig-more#rsa-sha256),与 SignatureMethod.RSA_SHA256 对应。
  • Reference URI="" 配合 enveloped-signature 转换:摘要计算时剔除 Signature 自身,避免自引用死循环。
  • DigestMethod:SHA256(http://www.w3.org/2001/04/xmlenc#sha256);DigestValue 是文档(除 Signature 外)规范化后的摘要。
  • SignatureValue:用 RSA 私钥对 SignedInfo 签名后的值;KeyInfo/KeyValue/RSAKeyValue 内嵌了公钥模数与指数,验签方可直接取用。

7.2 验签输出

signature valid: true 表示核心验证(引用验证 + 签名验证)全部通过:文档未被改动,且 SignatureValue 可用公钥验证成立。ref[0] validity status: true 表示第 0 个 Reference 的摘要验证通过。

7.3 篡改验签输出

金额从 12800.00 改为 99999.00 后,文档摘要与 DigestValue 不再匹配,signature valid: false。这正是 XML 签名的防篡改语义:任何对被覆盖内容(URI="" 覆盖整个文档)的改动都会导致验签失败。

八、方案 B:直接使用 jar 包运行(可选)

无 Maven 环境时,可用 javac/java 配合 5 个 jar 直接编译运行(jar 清单与下载见《Apache Santuario 安装指南》)。

8.1 编译示例类

javac -encoding UTF-8 -cp '/opt/xmlsec/lib/*' -d /opt/xmlsec/classes /root/xmlsec-demo/src/main/java/org/example/SignExample.java /root/xmlsec-demo/src/main/java/org/example/ValidateExample.java

编译成功无任何输出。

8.2 运行签名与验签

把 invoice.xml 复制到独立工作目录后运行:

cp /root/xmlsec-demo/invoice.xml /root/xmlsec-cp-test/
cd /root/xmlsec-cp-test
java -cp '/opt/xmlsec/classes:/opt/xmlsec/lib/*' org.example.SignExample
java -cp '/opt/xmlsec/classes:/opt/xmlsec/lib/*' org.example.ValidateExample

实测回显与 Maven 方式一致——签名:

$ cd /root/xmlsec-cp-test && java -cp '/opt/xmlsec/classes:/opt/xmlsec/lib/*' org.example.SignExample
[Info] RSA 2048 keypair generated: private.key / public.key
[Info] XMLSignatureFactory provider: ApacheXMLDSig - Apache Santuario XMLDSig (DOM XMLSignatureFactory; DOM KeyInfoFactory; C14N 1.0, C14N 1.1, Exclusive C14N, Base64, Enveloped, XPath, XPath2, XSLT TransformServices)
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[Info] document signed, Signature appended to root element
[Info] signed document written to signed.xml

验签:

$ cd /root/xmlsec-cp-test && java -cp '/opt/xmlsec/classes:/opt/xmlsec/lib/*' org.example.ValidateExample
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
signature valid: true
ref[0] validity status: true

8.3 JVM 堆参数生效验证

用 Maven 生成依赖 classpath 文件后以 -Xms256m -Xmx512m 直跑,并用 jps -v 确认:

$ mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
$ head -c 200 cp.txt
/root/.m2/repository/org/apache/santuario/xmlsec/3.0.6/xmlsec-3.0.6.jar:/root/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar:/root/.m2/repository/jakarta/xml/bind/jakarta.xml.bind-api/
$ java -Xms256m -Xmx512m -cp target/classes:$(cat cp.txt) org.example.ValidateExample &
$ jps -v | grep -F 'Xmx512m'
6855 ValidateExample -Xms256m -Xmx512m

jps -v 输出中的 -Xms256m -Xmx512m 即堆参数生效标记(进程号以实际为准),该次运行最终输出同样为 signature valid: true 与 ref[0] validity status: true。

九、注意事项

⚠️ 注意:

  1. DocumentBuilderFactory 必须调用 setNamespaceAware(true),否则无法按命名空间定位 Signature 节点,验签会失败。
  2. 序列化 signed.xml 时不要启用 indent:缩进会改变文档字节内容,破坏摘要一致性,导致验签失败。
  3. SignExample 每次运行都会覆盖密钥文件与 signed.xml;验签必须使用签名同一次运行生成的公钥。
  4. 生产验签务必开启 secure validation(org.apache.jcp.xml.dsig.secureValidation 置 TRUE),以启用算法白名单与 Transform 数量限制,详见《Apache Santuario 配置参数详解》。
  5. 示例用 RSA 2048 + SHA256 组合,是 secure validation 下的安全默认选择;MD5 等弱算法会被拒绝。

十、常见问题

Q1:provider 回显是 XMLDSig 而不是 ApacheXMLDSig? 说明 XMLSignatureFactory.getInstance("DOM") 未显式传入 Santuario 提供者,解析到了 JDK 内置实现。按示例写法 getInstance("DOM", new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI()) 即可,诊断过程见《Apache Santuario 常用工具与 API 详解》。

Q2:signature valid: false 的常见原因有哪些? 按命中率排查:文档在签名后被修改(含空格、换行、缩进变化);签名与验签使用了不同密钥对;序列化时启用了 indent;解析时未开启 namespace aware。

Q3:SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder" 报错吗? 不是错误。xmlsec 依赖 slf4j-api 但未绑定日志实现,SLF4J 回退到 NOP 日志,不影响签名与验签功能。