
使用 itext 5 对 pdf 进行多次数字签名时,常见问题在于后续签名会覆盖前序签名——根本原因并非代码逻辑错误,而是 pdf 在签名之间被外部工具(如 libreoffice、acrobat“保存”或某些 pdf 库)意外扁平化(flattening),导致原有签名字段丢失。本文详解正确实现多签名的关键原则与实操步骤。
使用 itext 5 对 pdf 进行多次数字签名时,常见问题在于后续签名会覆盖前序签名——根本原因并非代码逻辑错误,而是 pdf 在签名之间被外部工具(如 libreoffice、acrobat“保存”或某些 pdf 库)意外扁平化(flattening),导致原有签名字段丢失。本文详解正确实现多签名的关键原则与实操步骤。
在 PDF 标准中,多次签名是完全支持的(即“增量更新 + 独立签名字段”),但前提是:每次签名操作都必须基于上一次签名后未经修改的原始字节流(即保留 PDF 的增量更新结构),且签名字段(/Sig 字典)不能被任何中间处理流程删除或固化(flatting)。你当前代码本身逻辑正确(使用 PdfStamper.createSignature(..., true) 启用了追加模式),问题出在签名流程之外的数据流转环节。
✅ 正确实现多签名的三大前提
- 输入 PDF 必须是“洁净”的原始文件:未被 LibreOffice、浏览器 PDF 预览器、旧版 Acrobat 或其他 PDF 工具“另存为”或“优化”。这类操作常触发字段扁平化,使已有签名仅剩视觉外观,失去数字签名结构。
-
每次签名必须基于前次签名输出的原始字节流:禁止将已签名 PDF 先写入磁盘再读取、禁止用
PdfReader重新解析已签名文件后再次PdfStamper(除非明确启用增量模式并确保未重写对象)。 -
签名字段需独立命名且位置不重叠:避免多个签名共用同一字段名(如默认
"Signature1"),否则后签会覆盖前签。应动态生成唯一字段名(如"Signature_20241105_001")。
? 修复后的核心代码示例(支持多次签名)
// ✅ 关键:始终基于上一次签名的 byte[] 直接构造 PdfReader,不经过任何中间转换
public byte[] signPdf(byte[] pdfBytes, byte[] img, String certBase64, String fieldName,
HttpHeaders headers, SignReq req, AuthorizationReq authReq)
throws IOException, DocumentException, GeneralSecurityException {
PdfReader reader = new PdfReader(pdfBytes); // ← 输入必须是上一轮输出的原始字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// ⚠️ 必须启用增量模式(true 第二个参数),否则会重写整个 PDF,破坏前序签名
PdfStamper stamper = PdfStamper.createSignature(reader, outputStream, ' ', null, true);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(Base64.getDecoder().decode(certBase64.replaceAll("\s", "")))
);
PdfSignatureAppearance sap = stamper.getSignatureAppearance();
sap.setReason("Approved by Finance Dept");
sap.setLocation("Ho Chi Minh City");
sap.setVisibleSignature(new Rectangle(100, 700, 250, 650), 1, fieldName); // ← 唯一字段名!
sap.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
ExternalDigest digest = new BouncyCastleDigest();
ExternalSignature signature = new CustomExternalSignature(headers, req, authReq, signatureServices);
MakeSignature.signDetached(sap, digest, signature, new Certificate[]{cert},
null, null, null, 0, MakeSignature.CryptoStandard.CMS);
stamper.close(); // 自动执行增量写入
reader.close();
return outputStream.toByteArray(); // ← 输出原始字节,供下一次签名直接使用
}
⚠️ 常见陷阱与规避建议
-
❌ 错误做法:
// 将签名后 PDF 保存为文件 → 用 File.open() 再读取 → 被系统或编辑器悄悄扁平化 Files.write(Paths.get("signed.pdf"), signedBytes); byte[] nextInput = Files.readAllBytes(Paths.get("signed.pdf")); // ❌ 风险极高! -
✅ 正确链式调用:
byte[] original = files.get(0).getBytes(); byte[] firstSigned = signPdf(original, ... , "Sig_A", ...); byte[] secondSigned = signPdf(firstSigned, ... , "Sig_B", ...); // ← 直接传入字节数组 byte[] thirdSigned = signPdf(secondSigned, ... , "Sig_C", ...);
-
验证签名完整性:使用 Adobe Acrobat 或
pdfsig(Poppler 工具)检查 PDF 是否包含多个/Sig字典:pdfsig signed_multiple.pdf # 输出应显示 >1 个签名条目,且状态均为 "Signature is VALID"
? 总结
iText 5 完全支持 PDF 多签名,其本质是利用 PDF 的增量更新机制(Incremental Update)在文件末尾追加新签名对象,而非覆盖原内容。失败的根本原因几乎总是外部工具对 PDF 的“无意识破坏”。因此,严格管控输入源、杜绝中间文件落地、确保字段命名唯一、全程使用内存字节数组流转,是实现可靠多签名的黄金准则。若仍遇到问题,请用 pdfinfo -meta 和 pdfsig 检查 PDF 元数据与签名结构,定位扁平化发生环节。










