☰
Java PDF签章实战:从数字签名到代码实现的完整方案
2026/10/11 13:21:04 网站建设 项目流程

简介:面向Java开发者的PDF电子签章实战示例包,聚焦借助iText、PDFBox等主流PDF处理库为文档添加可验证数字签名,适用于合同、证书等文件在线签章场景。压缩包共39个文件、约20.94MB,内含11个Java源码、11个class编译文件、7个jar依赖库、2个p12数字证书、2个PDF样例文档,以及工程配置与图片资源,源码和依赖齐全,导入Eclipse或IDEA即可运行调试。示例覆盖加载PDF、准备签名外观、读取密钥证书、执行签名并另存输出的完整调用链路,并展示可见签章的位置与样式自定义,能有效避开证书格式和API使用中的常见坑。包内目录按工程结构组织,关键代码处有注释,便于按需调整签名参数。该示例包已有2361人学习下载,适合刚接触PDF签章、希望快速获得可运行参考实现的Java工程师。

1. Java PDF签章到底做了什么:先分清“盖章”和“数字签名”

在Java后端给PDF“盖章”,很容易被一个表象带偏:以为把一张印章PNG贴到页面上就算签章。真正能拿到审计和存证环节里的,必须是“看得见的章”和“验得出的签名”两件事同时成立。PDF签章功能做的就是这件事:它在指定页面的指定位置画上印章外观,同时用私钥对文档内容做一次数字签名,阅读器打开时能验证文档从签章那一刻起有没有被动过。适合的场景很明确:合同在线签署、公文流转、招投标文件、电子回执这类对“谁签的、什么时候签的、是否被改过”有强诉求的系统。后端Java开发者、需要给现有系统集成签章能力的团队,都属于这个标题的目标人群。

2. 签章技术选型与核心链路:iText、PDFBox怎么选,一条签名流程如何走通

2.1 两个主力库的选型对比:iText系与PDFBox系

做选型之前,先把“我要的不只是贴图”这件事定下来。如果需求只是预览时有个红章,不做防篡改,那用PDFBox贴个图就行,半小时上线。但凡是合同、回执这类只要有人质疑“是不是你们自己P的”就要出事的场景,就必须上数字签名。到了这一步,Java里真正常用的方案集中在两类:iText系和PDFBox系,还有少量商业封装库,但授权模式更特殊,我一般只在快速验证时用。

下面这个对比表是我在新项目里会摆在会议桌上让团队拍板的依据:

方案许可模型签名API成熟度外观定制能力适合场景
iText 5.xAGPL/商业授权成熟,一条signDetached路径走通强:图片、描述文字、字体都可控存量项目多、教程多、要快速落地
iText 7.xAGPL/商业授权新API,类名和用法变化较大强新项目,但需要重新熟悉API
PDFBoxApache 2.0偏底层:ByteRange、PKCS#7都要自己组织一般对免费商用有硬要求的项目
商业封装库按年/按量收费简单中原型验证、非核心系统

只看表格容易觉得PDFBox更省心,毕竟Apache 2.0许可没有太大历史包袱。实际做下来你会发现,PDFBox把更多细节留给了你:构造PDSignature、计算ByteRange、组织PKCS#7数据,每一步都要对着PDF规范调试。iText 5把这条链路收敛成一个signDetached方法,外观、摘要、证书、签名值一次性写好,遇到问题网上能查到的案例也最多。所以我最常见的做法是:项目没有许可约束时优先走iText 5风格API,有严格免费商用要求时再考虑PDFBox,并且要预留两到三天的调试时间。

2.2 一条签章链路拆解:外观、摘要、证书三件事

不管选哪个库,一条完整的签章链路都由下面几件事组成。第一,外观层。你在页面上看到的红章其实是两层叠加:图片印章负责视觉,签名描述(谁签的、什么理由、什么时间)负责信息。第二,摘要与签名。文档的原始字节被哈希,私钥对哈希做加密,这步保证任何人改一个字节都能被识别出来。第三,证书链。公钥证书用来证明私钥持有者的身份,验证方拿证书里的公钥解开签名值。第四,容器格式。签名值、证书、摘要算法一起被打包成PKCS#7结构,写到PDF的签名字典里,配合ByteRange字段记录哪些字节是签过名的。

这里有一个新手最容易忽略的点:PDF签名不是把整份文件加密,它只记录一个字节范围。签名之后再做增量更新(比如盖第二枚章),原始字节没变,第一枚章依然有效;但如果哪个工具把PDF重新组织了一遍,哪怕内容看起来一模一样,验证也会失败。理解了这一点,很多诡异的报错就能解释通了。先把这个链路刻在脑子里,再去看代码,你会发现所有参数都是围绕这三件事展开的。

2.3 老代码里的setCrypto与新API signDetached

网上搜Java PDF签章,会看到大量appearance.setCrypto(pk, chain, null, ...)的写法。这是iText 5早期的API,能跑,但问题在于参数含义不直观,也不方便扩展OCSP和TSA。我写这篇文章时用的是MakeSignature.signDetached这条更清晰的路径:外部摘要器、外部签名器、证书链、CRL/OCSP列表、时间戳客户端、签名格式全部是显式参数。其中最后一个参数CryptoStandard.CMS是默认的PKCS#7/CMS格式,如果合规要求更严格,再换CryptoStandard.CADES并接入时间戳服务器。看到老代码先别急着抄,认准这两条API的差别,调参时才不会懵。

3. 用Java实现PDF签章:从p12证书、印章素材到完整签名代码

3.1 准备材料:p12证书、透明印章图与Maven依赖

动手前先准备三样东西。第一个是签名证书,开发阶段用keytool生成自签名PKCS#12证书最快,一条命令就能拿到可用的p12文件,生产环境再换成企业CA签发的证书:

keytool -genkeypair -alias signer -keyalg RSA -keysize 2048 \ -validity 3650 -storetype PKCS12 \ -keystore signer.p12 -storepass changeit \ -dname "CN=Signer Demo, OU=IT, O=Dev Org"

参数说明:-alias signer是证书在密钥库里的唯一名称,后面代码里加载私钥要用同一个名字;-storepass changeit是密钥库口令,代码里和keytool这里必须一致;-keysize 2048是目前比较稳妥的密钥长度,再配SHA-256摘要算法,满足绝大多数签章场景。自签名证书只适合开发联调,上线前要换成受信任CA签发的证书,否则客户端打开PDF会提示证书不受信任,这一点在后文避坑章节会专门讲。

第二个是印章图片。要求不高但很关键:透明背景PNG,红色印章导出时不要用白色底,不要合并图层。尺寸按显示尺寸的2倍导出,比如要显示4cm直径的章,图片就按8cm、300dpi出图,避免在PDF里被放大后锯齿明显。第三个是Maven依赖,iText 5.x和BouncyCastle各一个,版本按项目JDK匹配:

<dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix</artifactId> </dependency>

这里不写具体版本号,是因为iText 5.x和BouncyCastle的版本要跟项目里其他依赖对齐,硬套版本反而容易冲突。记住一个原则:BouncyCastle所有模块要用同一个版本族,否则运行期最常见的报错就是NoClassDefFoundError。

3.2 核心签章方法:完整代码与逐段说明

下面这个方法是我在实际项目里固定下来的一套签章工具,直接复制改参数就能用。代码按iText 5风格API编写,这也是网上存量教程最多的写法:

public static void signPdf(String src, String dest, String p12Path, String p12Password, String alias, byte[] sealPngBytes, int pageNum, Rectangle rect) throws Exception { // 1. 注册BouncyCastle Provider Security.addProvider(new BouncyCastleProvider()); // 2. 加载PKCS#12证书库,取私钥和证书链 KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(new FileInputStream(p12Path), p12Password.toCharArray()); PrivateKey privateKey = (PrivateKey) ks.getKey(alias, p12Password.toCharArray()); Certificate[] chain = ks.getCertificateChain(alias); // 3. 初始化签章外观,'\0'表示生成一个新的签名容器 PdfReader reader = new PdfReader(src); FileOutputStream fos = new FileOutputStream(dest); PdfStamper stamper = PdfStamper.createSignature(reader, fos, '\0'); PdfSignatureAppearance appearance = stamper.getSignatureAppearance(); // 4. 签名描述信息,会显示在阅读器的签名面板里 appearance.setReason("合同签署确认"); appearance.setLocation("在线签署平台"); appearance.setSignDate(new GregorianCalendar()); // 5. 可见签章区域:pageNum从1开始,fieldName必须全局唯一 appearance.setVisibleSignature(rect, pageNum, "sign_" + System.currentTimeMillis()); // 6. 印章图片与外观模式 Image seal = Image.getInstance(sealPngBytes); appearance.setSignatureGraphic(seal); appearance.setRenderingMode( PdfSignatureAppearance.RenderingMode.GRAPHIC_AND_DESCRIPTION); // 7. 中文显示层字体,没有itext-asian可换本地字体文件 BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); appearance.setLayer2Font(bf); // 8. 执行签名:摘要算法SHA-256,签名格式CMS ExternalSignature es = new PrivateKeySignature(privateKey, "SHA-256", "BC"); ExternalDigest digest = new BouncyCastleDigest(); MakeSignature.signDetached(appearance, digest, es, chain, null, null, null, 0, CryptoStandard.CMS); // 9. 必须在这里close,签名才会真正写入输出文件 stamper.close(); }

这段代码的逻辑和参数值得逐条说明。第2步加载密钥库时,ks.getKey(alias, p12Password)里的alias要和keytool生成时一致,否则拿不到私钥;证书链chain不能只传叶证书,要整条链一起传,验证方才能顺着链找到信任根。第5步的setVisibleSignature有三个关键参数:Rectangle定义章的位置和大小,pageNum是页码从1开始,fieldName是签名域的名字,重复会直接抛异常,所以我加了时间戳后缀。第6步的渲染模式GRAPHIC_AND_DESCRIPTION表示“图片+描述文字”都显示,如果只想显示一张干净的章,换成RenderingMode.GRAPHIC。第8步的CryptoStandard.CMS就是标准的PKCS#7,最后的0是预分配空间,传0让iText动态计算,多数场景够用。

有一个细节提醒:stamper.close()必须放在signDetached之后调用,签名才真正落盘。不少初学的人看到signDetached执行完就以为结束了,结果输出文件里只有外观没有签名数据,阅读器里显示“签名域为空”。把close养成习惯,签章这步就稳了一大半。

3.3 坐标、字段名与外观参数:最容易出错的三处

签章位置是翻车率最高的参数。PDF的坐标系统和屏幕坐标不一样:原点在页面左下角,单位为点(pt),1点等于1/72英寸。设计稿上你量到的是“距页面左边界4厘米、距下边界5厘米”,不能直接填进Rectangle,要换算:

float x = (float) (4.0 / 2.54 * 72); // 距左边界4cm -> 约113.4pt float y = (float) (5.0 / 2.54 * 72); // 距下边界5cm -> 约141.7pt float w = (float) (3.5 / 2.54 * 72); // 章宽3.5cm -> 约99.2pt float h = w; // 正方形章 Rectangle rect = new Rectangle(x, y, x + w, y + h);

Rectangle构造函数的四个参数是左下角x、左下角y、右上角x、右上角y,不是宽和高。很多翻车现场就是把后两个参数当成了宽高,导致章被拉得不成比例。再有就是带旋转的扫描PDF:如果页面元数据里有/Rotate 90,你按正常方向算好的坐标盖上去会偏移。排查方法很简单,先调reader.getPageRotation(pageNum)看有没有旋转值,有的话把坐标换算到旋转前的坐标系里。

字段名也有讲究。同一份PDF要盖多个章时,每次的fieldName不能重复;重复不是覆盖,是直接报“field already exists”。我习惯用“前缀+业务ID+序号”生成,比如sign_contract2024001_1、sign_contract2024001_2,既唯一又方便后面验证时定位。页码参数pageNum从1开始,不是从0开始,第一次写代码的人十个里有五个在这里栽过跟头。

4. PDF签章常见问题排查:5个坑,现象、原因与解决方案

4.1 打开PDF提示“签名有效性未知”:不是文档被篡改,是证书没被信任

现象:签章完成后,用Chrome或Adobe打开,左侧提示“签名有效性未知”,业务方第一时间以为是签章失败,反复要求重签。原因:自签名证书不在阅读器的信任根列表里。阅读器能验证“文档没被改过”,但无法确认“这个签名者可信”。解决:开发环境可以继续用自签名,上线前换成企业CA签发的证书;如果企业有自己的根证书,把根证书安装到客户端机器上也能消除红叉。这里要区分两个提示:如果看到的是“文档已被更改或签名无效”,那才是坏消息,优先怀疑签名后文档被其他工具另存过;如果只是“有效性未知”,问题在信任链,不在完整性。

4.2 印章落点偏了或跑出页面:PDF坐标原点在左下角

现象:按设计稿算好的位置盖下去,章跑到页面边缘甚至页面外。原因:把屏幕坐标系(原点左上角、单位像素)直接套到了PDF坐标系(原点左下角、单位点)上。解决:回到3.3的换算公式,从厘米或英寸换算成pt,并记住Rectangle是左下角+右上角的定义。我在项目里会写一个小的换算工具类,输入“距左、距下、宽、高”四个厘米值,输出Rectangle,团队所有人用同一个方法,基本根除这类问题。如果章还是偏,再看页面是否带旋转属性。

4.3 第二枚章把第一枚“盖失效”:增量更新与字段名唯一

现象:同一份PDF先盖甲方章,再盖乙方章。乙方章看着没问题,回头验甲方章时提示签名无效,或者盖第二枚时直接报字段重复。原因:第一枚签名后的PDF被当作全新文件重新保存了一遍,破坏了ByteRange;或者两个章共用了同一个fieldName。解决:链式签章,第二枚必须以第一枚的输出文件为输入,用PdfReader重新打开再做增量签名;fieldName每次生成唯一值。不要在两次签章中间用任何“压缩、优化、去水印”工具处理PDF,那些工具十有八九会重写字节,把签名弄失效。记住一个口诀:签名后的文件,只能继续签,不能重新编。

4.4 红色印章变成黑色方块:透明通道与渲染模式

现象:PNG印章盖上去,预览时红章变成了黑底或黑块,透明区域变成脏色。原因:印章图片的Alpha通道在渲染时没被正确处理,或者图片本身被转换成了不带透明信息的格式。解决:首先确认素材是PNG且带Alpha通道,JPEG格式大概率出问题;然后把渲染模式固定为GRAPHIC_AND_DESCRIPTION或GRAPHIC,避免使用某些默认模式下的兼容路径。如果图片本身没问题,换成白色背景也会出问题,那就检查是不是在导出图片时把透明底替换成了白底或黑底。这个坑排查起来很快,大部分情况是素材那条路出的问题。

4.5 中文签名信息显示成问号:字体、编码与itext-asian

现象:外观层显示“合同签署确认”变成“???”,元数据里的Reason字段反而正常。原因:PDF显示层默认字体不支持中文。解决:在代码里加上第7步的字体设置,用iTextAsian包的STSong-Light,或者加载本地系统字体:

BaseFont bf = BaseFont.createFont("/opt/fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); appearance.setLayer2Font(bf);

这里有个小坑:STSong-Light依赖itext-asian这个附加包,Maven里没引的话会报字体找不到。如果项目不方便加依赖,就换成本地字体文件的绝对路径,用BaseFont.IDENTITY_H编码。要注意的是,这个字体问题只影响显示层,不影响签名本身的法律效力,但业务方不买账,宁可提前处理。

5. 进阶:签名验证、批量签章与上线前最后一道检查

5.1 用iText自己验一遍签名:别等阅读器告诉你结果

签章功能上线前,最好在代码里留一个验证入口,能随时验一份PDF的签名状态。用iText的AcroFields能直接拿到签名列表并做完整性校验:

public static boolean verifySignature(String signedPdf) { PdfReader reader = new PdfReader(signedPdf); AcroFields fields = reader.getAcroFields(); List<String> names = fields.getSignatureNames(); boolean allValid = true; for (String name : names) { PdfPKCS7 pkcs7 = fields.verifySignature(name); allValid = allValid && pkcs7.verify(); System.out.println("签名字段: " + name + ", 时间: " + pkcs7.getSignDate().getTime() + ", 完整: " + pkcs7.verify()); } return allValid; }

这个方法验证的是“文档自签名后有没有被改动”,它回答不了“证书是不是可信”这个问题。实际业务里,完整性和可信性是两件事:完整性用这段代码验,可信性交给证书链去查。常见的做法是在自动化测试里把签章后的文件跑一遍verifySignature,再让阅读器打开确认UI无红叉,两道检查过了再发版。

5.2 批量签章的三个习惯

批量签章场景下有三件事容易踩。第一,循环里每次都要new新的PdfReader和FileOutputStream,不要复用实例,PdfReader和签章过程不是线程安全的,用线程池并发时尤其要注意,每个任务独占一套资源。第二,输出文件用临时文件策略:先写到tmp目录,全部成功后再统一改名,避免签一半进程挂掉把原文件毁了。第三,如果要盖多个章,严格按增量更新链处理,后一章读前一章的产物,不要回头去读原始文件。

我头一回在生产环境做签章时,就是没把证书链传全,某个阅读器里红叉一片,排查了大半天才意识到是链的问题。后来我把证书加载、坐标换算、签名、验证四段逻辑固定成同一个工具类,新项目直接复用,再没出过签章事故。PDF签章这个方向值得投入,前提是先把证书链路和验证手段跑通。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询