Java PDF签章实战:从数字签名到代码实现的完整方案

📅 发布时间:2026/10/11 13:21:04
Java PDF签章实战:从数字签名到代码实现的完整方案
简介面向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类名和用法变化较大强新项目但需要重新熟悉APIPDFBoxApache 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 CNSigner Demo, OUIT, ODev 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 groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId /dependency dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix/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的坐标系统和屏幕坐标不一样原点在页面左下角单位为点pt1点等于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(); ListString 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签章这个方向值得投入前提是先把证书链路和验证手段跑通。希望帮到你。本文还有配套的精品资源点击获取