资讯动态

iText 7 PDF处理实战:创建、文本替换、水印与数字签章全攻略

发布时间:2026/10/17 12:07:25 来源:尧图企业网站定制
简介面向 Java 开发者的 PDF 处理实用源码包聚焦 iText 库的四大核心操作创建文档、电子签章、斜字水印与文本替换。其中创建文档涵盖页面设置、字体与段落排版签章支持证书导入与签名区域定义水印可设置透明度及旋转角度文本替换则依赖表单域完成。资源基于 iText 5.5.11 与亚洲字体包并附带加密、证书等相关依赖库适合需要在业务系统中集成 PDF 生成、签署或内容修订的工程师参考学习。压缩包共三十二个文件包含七个 Java 源码与九个编译后的 class 文件、五个核心依赖库、两个 p12 证书、若干图片素材以及工程配置文件整体体积约 8.77MB。目录结构清晰说明文档对功能模块进行描述可快速导入开发环境运行演练。目前该源码包已有 1571 人学习从中可看到完整的 iText 操作 PDF 工程样例既提供可直接运行的代码实现也包含签名证书、水印图形等配套资源尤其对 PDF 签章与文本替换中的常见问题有直观演示能帮助使用者快速上手 iText避免重复踩坑提升开发效率。1. 为什么用 iText 处理 PDF一次说清创建、签章、水印、替换这四件事的关系后端只要沾到合同、报告、回执这类业务基本躲不开 PDF而 iText 是 JVM 生态里绕不过去的那一套库。标题里列的四件事——创建、签章、斜字水印、文本替换看起来是四个独立功能实际都建立在同一个 PdfDocument 和 PdfCanvas 模型上创建负责从无到有生成文档水印和文本替换是往页面内容流里追加或覆盖绘制指令签章则是在文件末尾加一段不可篡改的签名容器。很多团队在调研阶段最容易被“文本替换”带偏因为 iText 官方并没有一个 replaceText 方法网上能找到的代码又多是 iText 5 老 API拿 7.x 一跑就报 NoClassDefFoundError。这篇文章我会按一条可落地的链路讲先有一个能稳定输出中文 PDF 的工程再依次把替换、水印、签章加进去最后把最容易翻车的五个坑单独拎出来每条都给出现象和解决路径。适合正在做电子签章、批量报告生成、敏感词遮盖的 Java 开发者尤其是项目里已经翻过车、准备换方案的人。2. 创建 PDF最小 Java 工程与中文不乱的字体配置2.1 先分清 Document 和 PdfDocumentiText 7 的分层逻辑iText 5 时代一个 PdfWriter 加一个 Document 就能干活很多老教程到今天还能用但换到 iText 7 以后整个模型被拆成了 kernel 和 layout 两层。PdfDocument 属于 kernel 层负责页面、内容流、字体、注解这些底层对象Document 属于 layout 层负责把 Paragraph、Table、Image 这些高层的“块”自动排版到页面上。一句话记PdfDocument 是画板和画笔Document 是帮你排版的排版工。创建 PDF 时最常见的写法是先 new PdfWriter把它包进 PdfDocument再用 Document 包一层。两个对象都有 close 方法但关闭顺序有讲究必须先关 Document再关 PdfDocument。如果反过来Document 的缓存内容还没有刷进内容流输出文件就会少页或者整页空白。这个顺序问题在 Spring Boot 里尤其隐蔽因为容器关闭时资源清理顺序不可控最好在 finally 里显式按顺序关。PdfWriter writer new PdfWriter(out.pdf); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf); // 这里写段落、表格 document.close(); pdf.close();第二个参数是输出流也支持 OutputStream批处理场景建议直接用 ByteArrayOutputStream 或临时文件流避免直接写磁盘路径带来的权限问题。PdfWriter 还有一个带 StampingProperties 的构造专门用于读已有 PDF 再改写的场景第 3 章文本替换会用到。2.2 最小可运行 Demo从空白页到“中文不乱码”的段落很多第一次用 iText 7 的人会在中文字体上卡住。默认的 Helvetica 是标准 Type1 字体不包含中文字形直接写中文出来的 PDF 全是空白或者方框。解决路径有两条一是加载系统字体文件比如 Windows 的 simsun.ttc、Linux 的 NotoSansCJK二是用 iText 官方提供的 font-asian 包里面内置了 Adobe 的亚洲字体集合最常用的是 STSongStd-Light配合 UniGB-UCS2-H 编码就能输出简体中文。Maven 依赖里除了 itext7-core还要额外加 font-asian版本和 iText 主包保持一致。下面是最小可运行的工程结构dependency groupIdcom.itextpdf/groupId artifactIditext7-core/artifactId version7.2.5/version typepom/type /dependency dependency groupIdcom.itextpdf/groupId artifactIdfont-asian/artifactId version7.2.5/version /dependencyitext7-core 是一个聚合 POM会把 kernel、layout、io、signatures 等模块一起拉下来日常项目直接用这一个依赖就行。font-asian 单独引入因为它不是核心模块的默认依赖很多团队漏掉这一行然后死活找不到 STSongStd-Light。创建带中文段落的完整代码PdfFont font PdfFontFactory.createFont( STSongStd-Light, UniGB-UCS2-H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); PdfWriter writer new PdfWriter(hello.pdf); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf, PageSize.A4); Paragraph p new Paragraph(你好iText PDF 文档处理) .setFont(font) .setFontSize(14f) .setFontColor(ColorConstants.BLUE); document.add(p); document.close(); pdf.close();createFont 的第一个参数是 PostScript 字体名第二个参数是 CMAP 编码名。UniGB-UCS2-H 表示简体中文横向排版H 是 horizontalV 是 vertical。EmbeddingStrategy 建议用 PREFER_EMBEDDED这样即使对方机器没装中文字体PDF 打开也不会掉字。如果业务要求必须减小文件体积可以改成 PREFER_NOT_EMBEDDED但前提是目标环境一定有中文字体否则打印或预览时会替换字体导致排版错乱。2.3 页面尺寸、边距和坐标排版跑偏往往从这里开始创建 Document 时不传 PageSize默认是 A4 纵向。A4 的尺寸是 595 x 842 点1 点约等于 0.3528 毫米这个换算在画水印、签章矩形时经常用到。Document 的第二个构造参数是左右上下四个边距比如 new Document(pdf, PageSize.A4, 36, 36, 36, 36) 表示四边各留 36 点也就是 1.27 厘米左右的页边距。需要精确控制位置的内容不要依赖 Document.add而是用 Paragraph.setFixedPosition(pageNumber, x, y, width) 或者 Image.setFixedPosition。注意这里的 x、y 是页面的左下角坐标系不是常见的左上角。很多人从 HTML/CSS 思维转过来习惯性认为 y 越小越靠上结果元素跑到了页面外面。换算公式很简单页面内顶部位置 topY pageHeight - marginTop - elementHeight如果要从顶部往下 100 点开始画y 应该写 842 - 100 - elementHeight。Paragraph stamp new Paragraph(固定位置文本) .setFont(font) .setFixedPosition(1, 72, 720, 200); document.add(stamp);setFixedPosition 的第一个参数是页码从 1 开始。如果这段代码放在循环里给每一页都加内容页码参数要取当前页索引。另一个常见的误区是带着内容再去 new Document 套已有 PDFDocument 默认就是“新建文件 追加内容”如果你打算打开一个已有 PDF 往里插一段文字再用 Document.add会得到一个空白的覆盖文件。正确做法是第 3 章的 PdfReader PdfDocument 组合而不是 Document 直接包 PdfWriter很多老项目在这上面出过生产事故。3. 文本替换iText 没有现成 replaceText我用 Canvas 覆盖旧文本3.1 为什么官方不提供 replaceText APIPDF 打开后看起来是一个个可编辑的字符但底层文件里记录的只是“在某个坐标用某个字体绘制这些字形”的指令并没有保留文档级文本结构。所以替换文本不是一个查找替换操作而是要把旧的绘制指令“擦掉”在同一个位置绘制新的指令。这个“擦掉”又分两层一是视觉上覆盖掉旧内容二是内容流里旧文本对象仍可能被复制出来如果对安全性有要求必须用 redaction 方式真正清除内容流而不仅仅是画一个白底矩形。常见的做法有两类。一类是覆盖法在旧文本位置画一个不透明矩形盖住然后再写入新文本适合替换字符串长度接近、对内容流残留不敏感的场景。另一类是 redaction 法iText 的 pdfSweep 模块可以把指定文本区域从内容流中物理删除代价是性能开销大、坐标定位复杂。我在日常业务中默认用覆盖法因为大部分需求是把“张三”替换成“李四”或者在敏感字段上打码覆盖法足够。文本替换需要先回答一个问题怎么找到旧文本的坐标。iText 7 提供了一个内容解析器可以拿到页面里所有文本片段的渲染信息包括基线坐标、字号、宽度。下面是自定义的坐标收集器。class TextLocationExtractor extends LocationTextExtractionStrategy { ListTextRect locations new ArrayList(); Override public void renderText(TextRenderInfo renderInfo) { LineSegment baseline renderInfo.getBaseline(); float x baseline.getStartPoint().get(Vector.I1); float y baseline.getStartPoint().get(Vector.I2); float width renderInfo.getWidth(); float height renderInfo.getFontSize(); String text renderInfo.getText(); locations.add(new TextRect(x, y, width, height, text)); } }TextRenderInfo.getBaseline() 返回的是一段基线线段getStartPoint 取线段起点。这里有一个坐标系细节PDF 的 y 轴是自下而上的所以基线的 y 值是文字底部的坐标绘制水印或替换文本时的 setTextRenderingMode 和文本坐标都用这套逻辑。TextRect 是自己定义的 POJO用来保存位置信息。拿到所有文本片段后遍历匹配目标字符串。匹配逻辑要注意两点一是 PDF 的一行文字可能被拆成多个 TextRenderInfo尤其是有空格和中英文混排时二是如果目标文本跨行了就要按行拆解后逐行处理。我一般做的是单行精确匹配复杂需求会先按行聚合再匹配。try (PdfDocument pdfDoc new PdfDocument(new PdfReader(src))) { for (int i 1; i pdfDoc.getNumberOfPages(); i) { PdfPage page pdfDoc.getPage(i); TextLocationExtractor strategy new TextLocationExtractor(); PdfTextExtractor.getTextFromPage(page, strategy); for (TextRect rect : strategy.locations) { if (rect.text.contains(张三)) { replaceText(page, rect, 李四, font); } } } }这段代码里用 try-with-resources 管理 PdfDocument会自动关闭底层流比手动关闭省心。PdfReader 不会锁定文件所以同一个文件可以被重复读取但写入时要注意输出流不能和输入流指向同一个物理文件否则会报“流已关闭”或文件锁异常建议输出到临时文件再覆盖。3.2 覆盖旧文本自己画矩形防止“黑历史”残留定位到旧文本位置后下一步是在这个矩形区域上画一个不透明矩形把旧内容盖住再写入新文本。矩形颜色要和页面背景一致通常是白色但如果原 PDF 背景不是纯白或者表格单元格带底色就不能用白色得先读页面背景色。简单场景下可以直接用白色覆盖区域不会大到影响阅读。覆盖的时候矩形的高度不能只取字号因为中文字形上下都有留白行高一般要按字号乘以 1.2 到 1.4 来算。用基线 y 坐标作为矩形底边实际代码里我会给 y 减去一个偏移量让矩形覆盖从上一行底部到下一行顶部的范围否则容易露出残影。public void replaceText(PdfPage page, TextRect old, String newText, PdfFont font) { PdfCanvas canvas new PdfCanvas(page); canvas.saveState(); canvas.setFillColor(ColorConstants.WHITE); canvas.rectangle(old.x - 2, old.y - old.height * 0.2f, old.width 4, old.height * 1.4f); canvas.fill(); canvas.restoreState(); canvas.beginText(); canvas.setFontAndSize(font, old.height); canvas.setTextRenderingMode(PdfCanvas.TEXT_RENDER_MODE_FILL); canvas.setFillColor(ColorConstants.BLACK); float newWidth font.getWidth(newText, old.height); float startX old.x (old.width - newWidth) / 2; canvas.moveText(startX, old.y); canvas.showText(newText); canvas.endText(); }参数说明old.x 是旧文本左边界old.y 是基线old.height 是旧字号。rectangle 的宽度在原文本宽度上加 4是为了覆盖轻微的抗锯齿边缘高度用 1.4 倍字号避免上下的墨迹残留。newWidth 通过 PdfFont.getWidth 按实际字号计算新文本在旧区域水平居中视觉效果最接近原排版。replaceText 方法里用的是同一个 PdfCanvas 实例saveState 和 restoreState 成对出现确保覆盖矩形不会污染后续的内容流状态。这里有一个容易被忽略的坑覆盖用的 canvas 和写入新文本的 canvas 如果是两个对象内容流顺序可能会颠倒导致新文本先画、矩形后画结果把新文本也盖住了。所以必须用同一个 canvas 实例顺序是矩形在前文本在后。3.3 替换后文本长度不一致怎么办这是文本替换需求里最考验人的部分。替换“张三”为“李四”宽度差不多覆盖后正常但如果把“北京市朝阳区”替换成“北京市朝阳区某某街道某某号”新文本宽度可能超出旧区域。解决办法是按新文本实际宽度重新计算覆盖区域把矩形扩大同时检查是否超出页面边界或和其他文字重叠。float newWidth font.getWidth(newText, old.height); float rectWidth Math.max(old.width, newWidth) 4; float rectHeight old.height * 1.4f; canvas.rectangle(old.x - 2, old.y - old.height * 0.2f, rectWidth, rectHeight);如果替换后文本明显变长并且原区域右侧有其他重要内容扩大覆盖区域就会盖掉不该盖的字符。这种冲突没有万能解法常见做法是把新文本缩小字号或者把替换内容拆成两行二选一要看业务是否允许。还有一种做法是不做“替换”而是把旧文本打成马赛克然后把新文本写到旧文本下方很多电子合同里的修改痕迹就是这么做的既能保留变化记录又避免重叠。多页替换时建议用一个计数器记录每页处理了多少处替换如果某页目标文本出现多次循环里要每次重新获取该页的 PdfCanvas不能复用上一页的 canvas 实例。内容流写入完成后再调用 PdfDocument.close替换才会真正刷进输出文件。text-replacement 定位这块最容易踩的坑是 y 坐标和字号不匹配导致矩形盖在了文字上方或下方后面避坑章会专门谈到。4. 斜字水印旋转、透明度、平铺的实现与参数4.1 “斜字水印”到底指什么很多需求文档写“加斜字水印”字面理解是字体倾斜但实际交付时需求方要的往往是“斜向排布的大水印”也就是那种从左下角到右上角、贯穿整个页面的“内部资料”“复印件无效”。真正让水印有斜向感的是旋转角度而不是字体本身的 italic。iText 里设置斜体字需要字体文件自带 Italic 变体否则只能靠人工倾斜效果很差。所以我在实现时按“旋转 45 度”的方式来处理这也是行业里最普遍的做法。用 PdfCanvas 画水印优势是可以精确控制每个页面的绘制顺序。PdfCanvas 构造时可以指定内容流的位置PdfPage.NEXT_CANVAS 表示画在页面已有内容之后会盖住正文PdfPage.PREVIOUS_CANVAS 表示画在页面内容之前正文会盖住水印。水印一般要浮在文字之上所以用 NEXT_CANVAS透明度降到 0.15 到 0.25 之间既醒目又不影响阅读。PdfCanvas canvas new PdfCanvas(page, PdfPage.NEXT_CANVAS); canvas.saveState(); PdfExtGState gs new PdfExtGState().setFillOpacity(0.2f); canvas.setExtGState(gs); canvas.beginText(); canvas.setFontAndSize(font, 48); canvas.setColor(ColorConstants.LIGHT_GRAY, true); canvas.concatMatrix( (float) Math.cos(rad), (float) Math.sin(rad), (float) -Math.sin(rad), (float) Math.cos(rad), centerX, centerY); canvas.showText(内部资料); canvas.endText(); canvas.restoreState();concatMatrix 的六个参数对应 PDF 变换矩阵a、b、c、d 控制旋转和缩放e、f 控制平移。这里 e 和 f 传的是页面中心点坐标效果是先把坐标系平移到中心再绕这个点旋转 45 度。PdfCanvas.beginText 和 endText 之间只能写文本绘制命令写完 showText 再 restoreState画布状态不会影响后续内容。4.2 水印字体、大小、角度的参数设置水印字号没有固定标准我的习惯是取页面短边的 8% 到 12%。A4 短边是 595 点短边的 10% 就是 59.5 点取 48 到 60 之间视觉上刚刚好。字号太小像插画太大盖住正文字体。旋转角度建议取 30 到 60 度之间。45 度最平衡但长文本水印在 45 度时横向跨度最大容易超出页面边界。如果水印文字较长比如“内部资料禁止外传”八个字45 度会导致左上角和右下角有部分文字溢出页面这时要缩小字号或者改成 30 度。一个比较实用的计算方式是水印文字长度乘以字号得到文本宽度再按对角线长度 842 点去反推最大字号。颜色方面水印一般用灰色或浅蓝色。setColor(ColorConstants.LIGHT_GRAY, true) 的第二个参数 true 表示填充色false 表示描边色。如果水印需要更淡的灰可以直接 new DeviceRgb(0.9f, 0.9f, 0.9f)不要用纯黑加很低透明度那样打印机出来的效果会不均匀。水印位置不能简单地从页面中心点开始写。showText 是从当前文本坐标开始绘制的文本的左下角会落在 centerX, centerY 这个点如果不做偏移整行水印会偏在页面右上方。我一般会在旋转前先把原点平移到页面中心再把文本坐标左移半个文本宽度、下移半个字号这样水印才真正居中。平移放进 concatMatrix 的 e、f 参数里。float textWidth font.getWidth(内部资料, size); float x (PageSize.A4.getWidth() - textWidth) / 2; float y (PageSize.A4.getHeight() - size) / 2; canvas.concatMatrix( (float) Math.cos(rad), (float) Math.sin(rad), (float) -Math.sin(rad), (float) Math.cos(rad), x, y);4.3 全页平铺水印密度和间隔怎么调单页一个大水印只适合封面正文每一页都需要全页平铺的水印样式通常是每条水印间隔固定距离横向排成几排。平铺的关键是间隔参数横向间隔建议取水印宽度的 1.2 倍纵向间隔取字号加上 120 到 180 点。间隔太小会有密集恐惧间隔太大又显得稀稀拉拉。实际上 A4 页面 45 度斜排三行四列是比较舒服的密度。for (int i 1; i pdfDoc.getNumberOfPages(); i) { PdfPage page pdfDoc.getPage(i); PdfCanvas canvas new PdfCanvas(page, PdfPage.NEXT_CANVAS); canvas.saveState(); PdfExtGState gs new PdfExtGState().setFillOpacity(0.15f); canvas.setExtGState(gs); for (int row -1; row 3; row) { for (int col -1; col 4; col) { float x col * 220f; float y row * 160f - 100f; canvas.beginText(); canvas.setFontAndSize(font, 40); canvas.concatMatrix( (float) Math.cos(rad), (float) Math.sin(rad), (float) -Math.sin(rad), (float) Math.cos(rad), x, y); canvas.showText(内部资料); canvas.endText(); } } canvas.restoreState(); }平铺时行和列的起点最好从负值开始覆盖页面边缘否则左上角和右下角会出现空白三角区。这里 row、col 的范围 -1 到 3/4比实际需要的行列数多一圈目的就是把溢出部分留出裁剪空间。conatMatrix 旋转之后原本规则的网格会跟着旋转所以间隔要根据旋转后的视觉间隔去试调20% 的误差不会有太大影响。水印性能也是要注意的点。每页画 20 条水印每个水印都 beginText/endText性能最差。优化方式是只做一个 beginText中间用 moveText 调整坐标只在画不同的旋转角度时才重新 concatMatrix。文档是十页以内时无所谓到了几百页的批量报告性能差别能到一倍。用 moveText 调整的是文本光标位置不会重置变换矩阵适合在同一旋转角度下画多条水印。5. 避坑5 个容易翻车的现象、原因与解决办法5.1 中文乱码PDF 打开是方框现象生成的 PDF 在浏览器预览正常但发给同事后用 Adobe Reader 打开中文全变成方框或者干脆不显示。原因用了系统字体但没嵌入或者用了标准 Type1 字体写中文。第二个原因最常见很多团队从 iText 5 迁移到 7 时代码里保留着 BaseFont.createFont(STSongStd-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED)但丢掉了 font-asian 依赖导致字体加载失败后静默回退到 Helvetica。解决引入 font-asian并显式指定 EmbeddingStrategy.PREFER_EMBEDDED。如果用的是 Windows 字体文件最好复制到项目 resources 目录再加载不要直接依赖服务器系统路径因为部署环境经常没有中文字体。5.2 文本替换后新文字盖住了旁边的内容现象把“张三”换成“李四”没问题但把“张三”换成“张三四五六”时新文字溢出盖住了右侧的正文。原因覆盖矩形宽度是按旧文本算的新文本比旧文本宽。解决替换前用 PdfFont.getWidth(newText, fontSize) 计算新文本宽度取新旧文本宽度的较大值作为矩形宽度。如果矩形扩大后触碰到右侧其他文字说明这段文本不适合原地替换需要改成缩小字号或者移动到下一行。这是文本替换方案里最考验现场判断的部分核心原则是“宁多盖、勿漏盖”多出来的空白至少不影响安全性漏盖就会暴露敏感信息。5.3 水印旋转中心跑偏现象水印文字旋转后出现在页面角落而不是以页面中心为轴旋转或者每条水印的位置毫无规律。原因对 concatMatrix 的变换顺序理解反了。concatMatrix 的参数是把新坐标系变换到当前坐标系不是“先旋转再平移”的直觉顺序。解决把页面中心的坐标直接放进 e、f 参数再把文本绘制坐标调整到页面中心。如果旋转前已经设置了文本坐标那么旋转后再平移会导致文本在旋转后的坐标系里叠加偏移。建议统一改成这次的水印写法一组 concatMatrix 完成旋转和平移后续不再叠加 moveText除非你刻意要画多条间隔水印。5.4 签章后文档被提示“自签名无效”现象签章后打开 PDFAdobe 提示签名无效或者看不到签名字段。原因最常见的是签名后又被 iText 重新打开保存过一次PdfSigner 的签名容器会被二次打开后的内容流改动破坏其次是自签名证书本身没有受信任根Adobe 会提示“证书不受信任”这跟签章代码无关。解决签章必须是处理链的最后一环签完的 PDF 不要再用任何代码打开保存。如果你需要把其他元数据写进去必须在签章之前完成。受信任问题需要把自签名证书导入系统证书库或者使用受信任的 CA 签发证书这属于部署层面的准备不是代码能解决的。5.5 大批量 PDF 内存溢出现象一次处理几百份 PDF每份几 MB结果 JVM 堆溢出。原因PdfReader 默认是把整个文件读进内存PdfWriter 输出到 ByteArrayOutputStream 时又把整个文件再放一份在内存两个大对象叠加堆不够很正常。解决能传文件路径就不要传 byte[]PdfReader 和 PdfWriter 都支持直接读文件流output 端用临时文件或 FileOutputStream如果必须用内存流处理完一个文档立刻置空引用不要用一个 Listbyte[] 囤住所有结果。还有一个习惯问题每份 PDF 处理完后要显式 close不能等 GC否则文件句柄堆积Windows 上会直接报“文件被占用”。6. 进阶数字签章的完整链路与验证习惯6.1 证书与 Maven 依赖签章不是简单地把一张图片“贴”到 PDF 上它是在 PDF 文件末尾写入一个 PKCS#7/CMS 签名容器里面包含签名值、证书链、摘要算法等信息。iText 的 signDetached 方法封装了大部分细节但底层依赖 BouncyCastle所以签章功能除了 itext7-core还要引入 bcprov 和 bcpkix 两个依赖。证书可以用 .p12/.pfx 文件也可以用 HSM 设备日常测试最方便的是用 keytool 生成自签证书再用 KeyStore 加载。dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version /dependency dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk15on/artifactId version1.70/version /dependency6.2 最小签章代码与参数签章流程分四步加载证书、创建 PdfSigner、配置签章外观、执行 signDetached。下面是完整的最小代码KeyStore ks KeyStore.getInstance(PKCS12); try (InputStream is new FileInputStream(keystore.p12)) { ks.load(is, password.toCharArray()); } String alias ks.aliases().nextElement(); PrivateKey pk (PrivateKey) ks.getKey(alias, password.toCharArray()); Certificate[] chain ks.getCertificateChain(alias); PdfSigner signer new PdfSigner( new PdfReader(source.pdf), new FileOutputStream(signed.pdf), new StampingProperties()); PdfSignatureAppearance appearance signer.getSignatureAppearance(); appearance.setPageNumber(1); appearance.setPageRect(new Rectangle(36, 36, 200, 100)); appearance.setSignatureCreator(EnterpriseDoc); appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION); IExternalSignature signature new PrivateKeySignature(pk, SHA-256, BC); signer.signDetached(signature, chain, null, null, null, 0, PdfSigner.CryptoStandard.CMS);signDetached 的第三个参数是 CRL 列表第四个是 OCSP 客户端第五个是时间戳服务器地址测试环境都可以传 null生产环境建议至少配置时间戳否则证书过期后签名验证会失败。PdfSigner 的 StampingProperties 如果不带参数默认允许签名后再次打开但这会导致文档被 final 修改后签名失效我会在这里加一句生产环境请用 useAppendMode()让签名以增量方式写入保留原始内容这是电子合同行业的标准做法。6.3 签名后的验证习惯验证签名不能只看 Adobe 的绿标服务器端要做程序化验证。iText 的 SignatureUtil 可以按签名名称取出 PdfPKCS7然后校验摘要和证书链。PdfDocument pdfDoc new PdfDocument(new PdfReader(signed.pdf)); SignatureUtil util new SignatureUtil(pdfDoc); MapString, PdfPKCS7 signatures util.verifyAllSignatures(); for (Map.EntryString, PdfPKCS7 entry : signatures.entrySet()) { PdfPKCS7 pkcs7 entry.getValue(); boolean valid pkcs7.verifySignatureIntegrityAndAuthenticity(); System.out.println(签名 entry.getKey() valid); }我在生产环境还加过一道更保险的检查验证完签名后再计算一遍文档校验和记录到数据库。如果用户拿到的文件和当时签的不一致即使 PDF 签名校验通过也要人工复核。这个习惯帮我拦住过一次“文档在传输过程中被企业网盘自动加过水印”的事故那一次 PDF 签名本身是好的但业务内容已经不是原始版本。处理 PDF 这几年最大的教训就是永远不要对着 Adobe 的输出猜代码行为签章、文本替换这类操作必须用程序化手段做正向校验别省这五分钟。希望这篇笔记能帮你把 iText 这条处理链一次跑通。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑