Java实现PDF数字签名:添加与清除全攻略
今天,我们就来一期Java硬核实战,手把手教你如何用代码搞定PDF的数字签名!我们将使用业界最强大的两个库:iText 7(功能最全)和 Apache PDFBox(完全开源)。
🛠️ 核心原理简述
在写代码之前,先理清逻辑:
- 添加签名:读取PDF -> 加载数字证书(.p12/.jks)-> 计算文件哈希 -> 加密哈希值 -> 将签名数据写入PDF特定字段。
- 删除签名:注意! 数字签名的本质是防篡改。你不能像删除图片一样“擦除”签名而不破坏文件完整性。
- 正规做法:如果业务允许(如草稿阶段),可以通过代码移除签名字段(Field),但这会使原签名失效,本质上生成了一个新的未签名文档。
- 暴力做法:重新生成PDF内容(不推荐,会丢失元数据)。
📦 方案一:使用 iText 7 (推荐,功能强大)
iText 是处理PDF的行业标准,但请注意:iText 7 采用 AGPL 协议。如果你的项目是闭源商业项目,需要购买商业许可证,或者改用 OpenPDF。
1. 引入依赖
在 pom.xml 中添加:
<dependencies><!-- iText 7 核心 --><dependency><groupId>com.itextpdf</groupId><artifactId>itext7-core</artifactId><version>7.2.6</version> <!-- 请使用最新稳定版 --><type>pom</type></dependency><!-- BouncyCastle 加密提供者 (签名必备) --><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk18on</artifactId><version>1.78.1</version></dependency></dependencies>
2. 准备数字证书
你需要一个 .p12 或 .jks 格式的证书文件。如果没有,可以用 keytool 生成一个测试用的:
keytool -genkeypair -alias testuser -keyalg RSA -keystore keystore.p12 -storetype PKCS12 -validity 365
3. 代码实现:添加数字签名
import com.itextpdf.kernel.pdf.*;import com.itextpdf.signatures.*;import org.bouncycastle.jce.provider.BouncyCastleProvider;import java.io.FileInputStream;import java.io.FileOutputStream;import java.security.KeyStore;import java.security.Security;public class PdfSignerExample {public static void main(String[] args) throws Exception {// 1. 注册 BouncyCastle 提供者Security.addProvider(new BouncyCastleProvider());String src = "contract_draft.pdf";String dest = "contract_signed.pdf";String p12Path = "keystore.p12";String password = "your_password";// 2. 加载密钥库KeyStore ks = KeyStore.getInstance("PKCS12");try (FileInputStream fis = new FileInputStream(p12Path)) {ks.load(fis, password.toCharArray());}// 3. 获取私钥和证书链String alias = ks.aliases().nextElement();PrivateKey pk = (PrivateKey) ks.getKey(alias, password.toCharArray());java.security.cert.Certificate[] chain = (java.security.cert.Certificate[]) ks.getCertificateChain(alias);// 4. 执行签名signPdf(src, dest, pk, chain, "Reason: Approved", "Location: Beijing");System.out.println("签名成功!");}private static void signPdf(String src, String dest, PrivateKey pk,java.security.cert.Certificate[] chain,String reason, String location) throws Exception {PdfReader reader = new PdfReader(src);FileOutputStream fos = new FileOutputStream(dest);PdfSigner signer = new PdfSigner(reader, fos, new StampingProperties());// 创建签名外观(可视化的签名框)IExternalSignature pks = new PrivateKeySignature(pk, DigestAlgorithms.SHA256, "BC");// 设置签名位置:第1页,坐标 (x,y, width, height)signer.signDetached(pks, chain, null, null, null, 0,PdfSigner.CryptoStandard.CMS, "TestUser", reason, location);// 如果需要可视化签名域,需配合 PdfFormCreator 使用,此处为简化演示隐形签名或默认外观fos.close();reader.close();}}
4. 代码实现:移除签名(清除签名字段)
再次强调:移除签名意味着文件不再具备法律效力,通常用于“撤回重签”场景。
import com.itextpdf.kernel.pdf.*;import com.itextpdf.forms.PdfAcroForm;import com.itextpdf.forms.fields.PdfFormField;public class RemoveSignatureExample {public static void main(String[] args) throws Exception {String src = "contract_signed.pdf";String dest = "contract_unsigned.pdf";PdfDocument pdfDoc = new PdfDocument(new PdfReader(src), new PdfWriter(dest));PdfAcroForm acroForm = PdfAcroForm.getAcroForm(pdfDoc, true);// 获取所有表单字段java.util.Map<String, PdfFormField> fields = acroForm.getFormFields();for (String fieldName : fields.keySet()) {PdfFormField field = fields.get(fieldName);// 判断是否为签名字段if (field.getFieldType().equals(PdfFormField.SIG)) {System.out.println("正在移除签名字段: " + fieldName);// 从表单中移除该字段acroForm.removeField(fieldName);}}pdfDoc.close();System.out.println("签名已清除,文件已保存为未签名状态。");}}
📦 方案二:使用 Apache PDFBox (完全开源)
如果你担心 AGPL 协议问题,Apache PDFBox 是最佳选择(Apache 2.0 协议)。
1. 引入依赖
<dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>3.0.1</version> <!-- PDFBox 3.x 支持更好的签名 --></dependency><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk18on</artifactId><version>1.78.1</version></dependency>
PDFBox 的签名逻辑类似,但 API 风格不同。需要使用 PDSignature 类。
// 伪代码示例try (PDDocument document = PDDocument.load(new File("input.pdf"))) {PDSignature signature = new PDSignature();signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);signature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED);signature.setReason("Approval");signature.setLocation("Beijing");// 设置签名时间signature.setSignDate(Calendar.getInstance());// 创建签名接口实现SignatureInterface signInterface = new MySignatureInterface(privateKey, certChain);document.saveIncremental(new FileOutputStream("output_signed.pdf"), signature, signInterface);}
(注:PDFBox 移除签名同样是通过遍历 PDAcroForm 并移除 PDSignature 字段实现)
⚠️ 开发中的“坑”与注意事项
-
文件锁问题:
在使用 iText 5 或旧版本时,PdfReader可能会锁定文件导致无法删除。
解决方案:始终使用FileInputStream构造 Reader,并在操作完成后显式关闭,或使用try-with-resources语法。 -
中文字体显示:
签名后的可见区域如果包含中文,必须加载中文字体(如SimHei.ttf或NotoSansCJK),否则签名处会显示乱码方框。 -
// iText 7 加载字体示例PdfFont font = PdfFontFactory.createFont("fonts/SimHei.ttf", PdfEncodings.IDENTITY_H, true); -
多重签名:
PDF 支持多人依次签名(增量更新)。上述代码默认是覆盖模式还是追加模式,取决于StampingProperties的设置。若要保留前一个人的签名,需配置为append模式。 -
法律合规性:
代码生成的签名是否具备法律效力,取决于证书的颁发机构(CA)。自签名证书(Self-signed)仅能证明“文件未被修改”,不能证明“签署者身份”。正式业务请对接权威 CA 接口。
🚀 结语
通过 Java 实现 PDF 数字签名,可以将繁琐的人工签署流程自动化,极大提升业务效率。无论是使用功能强大的 iText,还是完全开源的 PDFBox,核心都在于对数字证书的管理和对PDF结构的理解。
代码虽好,安全更重要! 请务必妥善保管你的私钥文件(.p12),切勿硬编码在代码库中哦!
夜雨聆风