乐于分享
好东西不私藏

Java实现PDF数字签名:添加与清除全攻略

Java实现PDF数字签名:添加与清除全攻略

今天,我们就来一期Java硬核实战,手把手教你如何用代码搞定PDF的数字签名!我们将使用业界最强大的两个库:iText 7(功能最全)和 Apache PDFBox(完全开源)。


🛠️ 核心原理简述

在写代码之前,先理清逻辑:

  1. 添加签名:读取PDF -> 加载数字证书(.p12/.jks)-> 计算文件哈希 -> 加密哈希值 -> 将签名数据写入PDF特定字段。
  2. 删除签名注意! 数字签名的本质是防篡改。你不能像删除图片一样“擦除”签名而不破坏文件完整性。
    • 正规做法:如果业务允许(如草稿阶段),可以通过代码移除签名字段(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, nullnullnull0            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>
2. 核心代码逻辑 (简略版)

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 字段实现)


⚠️ 开发中的“坑”与注意事项

  1. 文件锁问题
    在使用 iText 5 或旧版本时,PdfReader 可能会锁定文件导致无法删除。
    解决方案:始终使用 FileInputStream 构造 Reader,并在操作完成后显式关闭,或使用 try-with-resources 语法。

  2. 中文字体显示
    签名后的可见区域如果包含中文,必须加载中文字体(如 SimHei.ttf 或 NotoSansCJK),否则签名处会显示乱码方框。

    // iText 7 加载字体示例PdfFont font = PdfFontFactory.createFont("fonts/SimHei.ttf", PdfEncodings.IDENTITY_H, true);
  3. 多重签名
    PDF 支持多人依次签名(增量更新)。上述代码默认是覆盖模式还是追加模式,取决于 StampingProperties 的设置。若要保留前一个人的签名,需配置为 append 模式。

  4. 法律合规性
    代码生成的签名是否具备法律效力,取决于证书的颁发机构(CA)。自签名证书(Self-signed)仅能证明“文件未被修改”,不能证明“签署者身份”。正式业务请对接权威 CA 接口。


🚀 结语

通过 Java 实现 PDF 数字签名,可以将繁琐的人工签署流程自动化,极大提升业务效率。无论是使用功能强大的 iText,还是完全开源的 PDFBox,核心都在于对数字证书的管理和对PDF结构的理解。

代码虽好,安全更重要! 请务必妥善保管你的私钥文件(.p12),切勿硬编码在代码库中哦!

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Java实现PDF数字签名:添加与清除全攻略

猜你喜欢

  • 暂无文章