You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用PDFBOX Java实现带签名PDF表单扁平化,解决签名仍可删除的问题

问题排查与解决方案:PDFBox扁平化带签名表单后仍可修改的问题

首先,咱们拆解下你遇到的核心问题:用PDFBox处理带签名的PDF表单后,签名依然能被删除、文档还能修改。这主要源于代码逻辑的错误,以及PDF签名的特殊性和文档权限未锁定这两方面原因。

一、当前代码的问题分析

  1. 错误的AcroForm初始化逻辑
    当原文档的acroForm为null时,你重新加载了一个新的PDDocument来创建PDAcroForm,这会导致新创建的AcroForm和原文档的资源完全脱节,后续的扁平化操作根本没作用在原文档上,这是一个关键bug。

  2. 未处理签名注解
    PDF签名是以**注解(Annotation)**的形式存在于页面中的,普通的表单字段扁平化不会处理签名注解,所以签名依然是可编辑/删除的独立元素。

  3. 未设置文档修改权限
    即使完成了扁平化,你没有对PDF进行加密和权限设置,任何人都可以用PDF编辑工具修改内容。

二、可行的解决方案

根据你“生成完全不可修改的PDF”的需求,需要分三步处理:

  1. 修正扁平化代码逻辑,正确处理原文档的表单字段
  2. 将签名注解转换为静态内容(注意:这会失去签名的可验证性,因为签名是基于原始文档哈希的,修改后签名会失效;如果需要保留签名有效性,跳过这步,直接设置权限)
  3. 对PDF进行加密,锁定所有修改权限

修正后的完整代码

import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.acroform.PDAcroForm;
import org.apache.pdfbox.pdmodel.acroform.PDField;
import org.apache.pdfbox.pdmodel.acroform.PDNonTerminalField;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignatureField;
import org.apache.pdfbox.pdmodel.security.*;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class PDFFlattener {

    private static void flattenPDFWithLock(String src, String dst, String ownerPassword) throws IOException {
        PDDocument doc = null;
        try {
            doc = PDDocument.load(new File(src));
            PDDocumentCatalog catalog = doc.getDocumentCatalog();
            PDAcroForm acroForm = catalog.getAcroForm();

            // 如果存在表单字段,处理并扁平化
            if (acroForm != null) {
                PDResources resources = acroForm.getDefaultResources();
                if (resources == null) {
                    resources = new PDResources();
                }
                List<PDField> fields = new ArrayList<>(acroForm.getFields());
                processFields(fields, resources);
                acroForm.setDefaultResources(resources);
                acroForm.flatten();
            }

            // 将签名注解转换为静态内容(可选:如果不需要保留签名验证性则启用)
            convertSignaturesToStaticContent(doc);

            // 设置文档权限:禁止所有修改操作
            setDocumentPermissions(doc, ownerPassword);

            // 保存文档
            doc.save(dst);
        } catch (IOException e) {
            System.err.println("处理PDF时出错: " + e.getMessage());
            throw e;
        } finally {
            if (doc != null) {
                doc.close();
            }
        }
    }

    private static void processFields(List<PDField> fields, PDResources resources) {
        fields.forEach(f -> {
            try {
                // 设置字段为只读并确保值正确
                f.setReadOnly(true);
                String value = f.getValueAsString();
                if (value != null) {
                    f.setValue(value);
                }

                // 递归处理子字段
                if (f instanceof PDNonTerminalField) {
                    processFields(((PDNonTerminalField) f).getChildren(), resources);
                }
            } catch (IOException e) {
                if (e.getMessage().matches("Could not find font: /.*")) {
                    String fontName = e.getMessage().replaceAll("^[^/]*/", "");
                    System.out.println("添加 fallback 字体: " + fontName);
                    resources.put(COSName.getPDFName(fontName), PDType1Font.HELVETICA);
                    try {
                        f.setValue(f.getValueAsString());
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                } else {
                    e.printStackTrace();
                }
            }
        });
    }

    private static void convertSignaturesToStaticContent(PDDocument doc) throws IOException {
        for (PDPage page : doc.getPages()) {
            List<PDAnnotationWidget> widgetsToRemove = new ArrayList<>();
            for (PDAnnotationWidget widget : page.getAnnotations()) {
                if (widget.getParent() instanceof PDSignatureField) {
                    // 将签名注解渲染为静态的Form XObject
                    PDRectangle rect = widget.getRectangle();
                    PDFormXObject formXObject = new PDFormXObject(doc);
                    formXObject.setBBox(rect);
                    formXObject.getResources().addAll(page.getResources());

                    // 将签名的内容绘制到Form XObject
                    try (PDPageContentStream contentStream = new PDPageContentStream(doc, formXObject)) {
                        widget.getAppearance().getNormalAppearance().getAppearanceStream().draw(contentStream);
                    }

                    // 在页面上绘制这个静态内容
                    try (PDPageContentStream contentStream = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true)) {
                        contentStream.drawForm(formXObject, rect.getLowerLeftX(), rect.getLowerLeftY(), rect.getWidth(), rect.getHeight());
                    }

                    // 标记要移除的签名注解
                    widgetsToRemove.add(widget);
                }
            }
            // 移除原始的签名注解
            widgetsToRemove.forEach(page.getAnnotations()::remove);
        }
    }

    private static void setDocumentPermissions(PDDocument doc, String ownerPassword) throws IOException {
        StandardProtectionPolicy policy = new StandardProtectionPolicy(ownerPassword, null, new AccessPermission());
        // 禁止所有修改操作:包括编辑内容、注释、表单字段等
        AccessPermission permissions = policy.getPermissions();
        permissions.setCanModify(false);
        permissions.setCanModifyAnnotations(false);
        permissions.setCanFillInForm(false);
        permissions.setCanAssembleDocument(false);
        permissions.setCanExtractContent(false);
        permissions.setCanExtractForAccessibility(false);
        // 可选:允许打印(如果需要)
        // permissions.setCanPrint(true);

        // 设置加密强度
        policy.setEncryptionKeyLength(256);
        doc.protect(policy);
    }

    public static void main(String[] args) {
        try {
            flattenPDFWithLock("input.pdf", "output.pdf", "your-owner-password");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三、关键说明

  1. 签名处理的权衡

    • 如果启用convertSignaturesToStaticContent方法,签名会被转成静态图像/内容,此时文档完全不可修改,但签名无法再被验证(因为原始签名的哈希已经被破坏)。
    • 如果需要保留签名的可验证性,那么不要调用这个方法,仅通过setDocumentPermissions设置权限。此时虽然用户无法修改文档而不破坏签名,但专业工具仍可移除签名(但会导致签名失效)。
  2. 权限设置

    • 代码中设置了严格的权限,禁止所有修改操作,你可以根据需求调整(比如允许打印)。
    • ownerPassword是文档的所有者密码,拥有这个密码的用户可以修改权限;普通用户没有密码,只能按设置的权限操作。
  3. 代码修正点

    • 移除了原代码中重新加载文档创建AcroForm的错误逻辑,直接使用原文档的资源。
    • 优化了字段处理逻辑,直接使用getValueAsString()获取字段值,更可靠。

内容的提问来源于stack exchange,提问作者PereZix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 10:37:40