使用PDFBOX Java实现带签名PDF表单扁平化,解决签名仍可删除的问题
问题排查与解决方案:PDFBox扁平化带签名表单后仍可修改的问题
首先,咱们拆解下你遇到的核心问题:用PDFBox处理带签名的PDF表单后,签名依然能被删除、文档还能修改。这主要源于代码逻辑的错误,以及PDF签名的特殊性和文档权限未锁定这两方面原因。
一、当前代码的问题分析
错误的AcroForm初始化逻辑
当原文档的acroForm为null时,你重新加载了一个新的PDDocument来创建PDAcroForm,这会导致新创建的AcroForm和原文档的资源完全脱节,后续的扁平化操作根本没作用在原文档上,这是一个关键bug。未处理签名注解
PDF签名是以**注解(Annotation)**的形式存在于页面中的,普通的表单字段扁平化不会处理签名注解,所以签名依然是可编辑/删除的独立元素。未设置文档修改权限
即使完成了扁平化,你没有对PDF进行加密和权限设置,任何人都可以用PDF编辑工具修改内容。
二、可行的解决方案
根据你“生成完全不可修改的PDF”的需求,需要分三步处理:
- 修正扁平化代码逻辑,正确处理原文档的表单字段
- 将签名注解转换为静态内容(注意:这会失去签名的可验证性,因为签名是基于原始文档哈希的,修改后签名会失效;如果需要保留签名有效性,跳过这步,直接设置权限)
- 对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(); } } }
三、关键说明
签名处理的权衡
- 如果启用
convertSignaturesToStaticContent方法,签名会被转成静态图像/内容,此时文档完全不可修改,但签名无法再被验证(因为原始签名的哈希已经被破坏)。 - 如果需要保留签名的可验证性,那么不要调用这个方法,仅通过
setDocumentPermissions设置权限。此时虽然用户无法修改文档而不破坏签名,但专业工具仍可移除签名(但会导致签名失效)。
- 如果启用
权限设置
- 代码中设置了严格的权限,禁止所有修改操作,你可以根据需求调整(比如允许打印)。
ownerPassword是文档的所有者密码,拥有这个密码的用户可以修改权限;普通用户没有密码,只能按设置的权限操作。
代码修正点
- 移除了原代码中重新加载文档创建AcroForm的错误逻辑,直接使用原文档的资源。
- 优化了字段处理逻辑,直接使用
getValueAsString()获取字段值,更可靠。
内容的提问来源于stack exchange,提问作者PereZix
相关产品推荐
相关产品推荐

