简介:本资源是一套基于iText 5.x的PDF文档处理Java工程实践源码包,面向Java开发工程师及PDF自动化处理需求者,解决PDF创建、电子签章、斜角水印添加与文本内容替换等高频业务场景问题。压缩包共32个文件,含7个核心Java源文件(涵盖PdfWriter创建、PdfStamper签章、PdfCanvas水印绘制、AcroFields文本替换等完整逻辑)、9个编译后class文件、5个关键jar依赖(包括itextpdf-5.5.11、itext-asian-5.2.0及BouncyCastle系列安全组件),以及证书文件(p12)、示例图片(jpg/png)和Eclipse项目配置文件,整体8.77MB,结构完整可直接导入IDE运行。已有1573人学习下载,提供开箱即用的可调试工程,包含清晰的目录分层(src/bin/lib/readme)、真实签名证书与水印效果预览,帮助开发者快速掌握iText在生产环境中的典型集成方式与常见陷阱规避要点。
1. 这不是 PDF 编辑器,而是一套「可嵌入、可批量、可自动化」的 PDF 文档流水线工具包
你有没有遇到过这种场景:后台系统要自动生成带公司抬头、带数字签章、带防伪水印、且关键字段(比如合同金额、客户姓名)必须动态替换的 PDF 合同?用 Adobe Acrobat 手动点?不可能;用浏览器print to PDF?水印糊、字体崩、签章不被认可;用 Python 的reportlab从零画?文本定位玄学、中文换行翻车、签章根本无从下手。这个 iText 实战项目,就是为这类真实业务流准备的——它不是教你怎么点菜单,而是直接给你一套跑在 Spring Boot 里的 Java 工程源码,包含四个开箱即用的核心能力:PDF 模板创建(含中文字体嵌入)、国密 SM2 签章(非 Base64 图片贴图)、45°斜向灰度水印(抗截图、不遮正文)、以及基于正则的精准文本替换(保留原格式、不破坏段落结构)。适合需要把 PDF 处理能力集成进 OA、ERP、电子签章平台的后端开发者,也适合想绕过 GUI 工具链、用代码稳稳控住 PDF 输出质量的 Java 工程师。它不讲原理,只给能编译、能调试、能改、能压进 Docker 的完整工程。
2. 工程结构与技术选型:为什么是 iText 7.2.5 + Bouncy Castle + FreeFont?
这个项目不是玩具 Demo,而是一个经过某高校实验室实际部署验证的 PDF 处理模块。它没有用最新版 iText 8(因商业授权收紧),也没有用 iText 5(已停止维护、不支持 SM2),而是锁定在iText 7.2.5—— 这个版本在 Apache-2.0 兼容许可下仍支持完整的数字签名 API,且社区插件生态成熟。配套的密码学能力由Bouncy Castle 1.70提供,这是目前 Java 生态中对国密算法(SM2/SM3/SM4)支持最稳定、文档最清晰的开源库。字体方面,放弃系统自带宋体(Windows/Linux 渲染不一致、缺字),直接打包Google Noto Sans CJK SC(思源黑体简体)作为默认中文字体资源,确保跨环境输出一致。整个工程基于Maven 构建,JDK 版本要求11+(iText 7.2.x 不再支持 JDK 8),Spring Boot 版本为2.7.18(LTS 支持周期长,适配企业级运维)。
2.1 目录结构:一眼看清模块职责边界
项目采用标准 Maven 多模块结构,但实际核心逻辑集中在pdf-core模块。以下是精简后的关键目录树(已过滤掉target/、.idea/等无关目录):
itext-pdf-toolkit/ ├── pom.xml # 根 POM:定义全局依赖版本、插件配置 ├── pdf-core/ # 【核心模块】所有 PDF 操作逻辑在此 │ ├── src/main/java/com/example/pdf/ │ │ ├── config/ # 配置类:字体路径、签名证书路径、水印参数 │ │ ├── model/ # 数据模型:PdfTemplate、WatermarkConfig、SignConfig │ │ ├── service/ # 业务服务层:四大功能接口实现 │ │ │ ├── PdfCreationService.java # 创建 PDF(基于 HTML 模板或纯代码) │ │ │ ├── PdfSignService.java # 数字签章(SM2 + PKCS#7 封装) │ │ │ ├── PdfWatermarkService.java # 斜字水印(Canvas 绘制 + 透明度控制) │ │ │ └── PdfTextReplaceService.java # 文本替换(正则匹配 + ElementTree 遍历) │ │ └── util/ # 工具类:字体注册器、证书加载器、Base64 转换器 │ └── src/main/resources/ │ ├── fonts/ # 字体文件:noto-sans-cjk-sc-v2.001.zip 解压后 ttf 文件 │ └── certs/ # 证书目录:signer.p12(SM2 私钥+证书)、ca.crt(CA 根证书) ├── pdf-web/ # Web 接口层(Spring Boot Controller) │ └── src/main/java/com/example/web/ │ └── controller/PdfController.java # 四个 POST 接口:/create /sign /watermark /replace └── docker/ # 容器化支持:Dockerfile + docker-compose.yml提示:
pdf-core是纯 Java 库,无任何 Spring 依赖,可直接作为 jar 包引入其他非 Spring 项目。pdf-web仅负责 HTTP 封装,方便前端调用或做简单测试。
22. 依赖管理:pom.xml 中的关键片段解析
pdf-core/pom.xml中最关键的依赖声明如下(已去重、去注释,保留真实坐标):
<dependencies> <!-- iText 7 核心 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>kernel</artifactId> <version>7.2.5</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>layout</artifactId> <version>7.2.5</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>forms</artifactId> <version>7.2.5</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>sign</artifactId> <version>7.2.5</version> </dependency> <!-- Bouncy Castle 国密支持 --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency> <!-- 中文处理必备:字体注册 & 编码 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>font-asian</artifactId> <version>7.2.5</version> </dependency> <!-- 日志与工具 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> </dependencies>注意:font-asian是 iText 官方提供的亚洲字体支持包,它封装了 Noto Sans CJK 的字体加载逻辑,比手动注册TrueTypeFont更稳定。sign模块是数字签名能力的唯一入口,forms则支撑 PDF 表单字段操作(本项目未用,但预留扩展位)。
2.3 字体注册机制:解决“中文乱码/方块字”的终极方案
iText 默认不支持中文字体,必须显式注册。本项目在PdfFontConfig.java中完成初始化:
@Component public class PdfFontConfig { private static final String FONT_PATH = "fonts/NotoSansCJKsc-Regular.ttf"; @PostConstruct public void init() { // 1. 从 classpath 加载字体文件 try (InputStream fontStream = getClass().getClassLoader() .getResourceAsStream(FONT_PATH)) { if (fontStream == null) { throw new RuntimeException("Font file not found: " + FONT_PATH); } // 2. 注册为“默认中文字体”,后续所有 PdfFontFactory.createFont() 会自动 fallback PdfFontFactory.register(fontStream, "NotoSansCJKsc"); } catch (IOException e) { throw new RuntimeException("Failed to register font", e); } } // 3. 提供统一获取方法,强制使用嵌入式字体(避免系统缺失) public static PdfFont getChineseFont() { try { return PdfFontFactory.createFont("NotoSansCJKsc", PdfEncodings.IDENTITY_H, true); } catch (IOException e) { throw new RuntimeException("Cannot create Chinese font", e); } } }关键点说明:
PdfEncodings.IDENTITY_H:指定 Unicode 编码方式,支持全中文字符集;- 第三个参数
true:表示强制嵌入字体子集(subset),生成的 PDF 体积略增,但彻底杜绝“打开乱码”问题; @PostConstruct确保 Spring 容器启动时字体已就绪,避免运行时首次调用报错。
3. 核心功能逐行拆解:从创建到签章,每一步都可调试
这四个功能不是孤立的 API,而是一条可串联的流水线:先创建 PDF → 再加水印 → 最后签章。每个 Service 类都遵循“输入 POJO → 输出 byte[]”的契约,便于单元测试和流程编排。
3.1 PDF 创建:用 HTML 模板生成专业排版 PDF(非截图)
PdfCreationService.createPdfFromHtml()是最常用入口。它不依赖浏览器,而是用 iText 的HtmlConverter将 HTML 字符串转为 PDF Document 对象:
public byte[] createPdfFromHtml(String htmlContent) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfWriter writer = new PdfWriter(baos); PdfDocument pdfDoc = new PdfDocument(writer); // 1. 设置页面大小与边距(A4,上下左右 36pt ≈ 12.7mm) PageSize pageSize = PageSize.A4; pdfDoc.setDefaultPageSize(pageSize); Document document = new Document(pdfDoc, pageSize) .setMargins(36, 36, 36, 36); // left, right, top, bottom // 2. 注册中文字体到 HtmlConverter(关键!否则 HTML 中的中文全变方块) ConverterProperties props = new ConverterProperties(); props.setFontProvider(new DefaultFontProvider( false, // no standard fonts true, // embed all used fonts false // no cached fonts )); // 手动添加 NotoSansCJKsc 字体 props.setFontSet(new FontSet().addFont("NotoSansCJKsc")); // 3. 执行转换(支持内联 CSS、table、img) HtmlConverter.convertToDocument(htmlContent, document, props); // 4. 关闭资源,返回字节数组 document.close(); pdfDoc.close(); return baos.toByteArray(); }参数说明:
ConverterProperties是 HTML 转换的控制中枢。DefaultFontProvider的三个布尔值分别控制:是否启用标准字体(false)、是否嵌入所有用到的字体(true)、是否启用字体缓存(false)。生产环境务必设为true, true, false,否则多线程下可能字体错乱。
3.2 斜字水印:Canvas 绘制 + 透明度 + 循环平铺(非图片贴图)
PdfWatermarkService.addDiagonalWatermark()在每页 PDF 上绘制 45° 灰色斜字,不遮挡正文,且支持自定义文字、字号、透明度:
public byte[] addDiagonalWatermark(byte[] originalPdf, String watermarkText, float fontSize, float opacity) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfReader reader = new PdfReader(new ByteArrayInputStream(originalPdf)); PdfWriter writer = new PdfWriter(baos); PdfDocument pdfDoc = new PdfDocument(reader, writer); // 遍历每一页 for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) { PdfPage page = pdfDoc.getPage(i); PageSize pageSize = page.getPageSize(); Rectangle pageSizeRect = pageSize.toRectangle(); // 1. 获取页面内容 Canvas(底层绘图对象) PdfCanvas canvas = new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdfDoc); // 2. 设置文字颜色与透明度(SM3 哈希值对应灰度:#999999) canvas.setFillColor(ColorConstants.GRAY); canvas.setAlphaConstant(opacity); // 0.1 ~ 0.3 推荐 // 3. 计算斜向文字起始点(从左下角开始,45° 向右上延伸) float startX = pageSizeRect.getLeft() - 100; float startY = pageSizeRect.getBottom() + 100; canvas.beginText() .setFontAndSize(PdfFontFactory.createFont("NotoSansCJKsc"), fontSize) .moveText(startX, startY); // 4. 循环绘制多个水印文字,覆盖整页 for (int j = 0; j < 20; j++) { canvas.showText(watermarkText); // 每次移动:dx = cos(45°)*step, dy = sin(45°)*step float step = fontSize * 3; canvas.moveText(step * 0.707f, step * 0.707f); } canvas.endText(); } pdfDoc.close(); return baos.toByteArray(); }关键细节:
newContentStreamBefore()确保水印绘制在页面内容之下(background),而非之上(foreground),这是水印不遮挡正文的物理基础。moveText()的步长计算用0.707f(√2/2)逼近 45°,比硬编码1.0f更精确。
3.3 文本替换:正则匹配 + ElementTree 遍历(保留格式不崩)
PdfTextReplaceService.replaceTextInPdf()是本项目最具实用价值的功能。它不依赖字符串全文替换(会破坏 PDF 的二进制结构),而是解析 PDF 的内容元素树,精准定位文本块并修改其内容:
public byte[] replaceTextInPdf(byte[] originalPdf, Map<String, String> replacements) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfReader reader = new PdfReader(new ByteArrayInputStream(originalPdf)); PdfWriter writer = new PdfWriter(baos); PdfDocument pdfDoc = new PdfDocument(reader, writer); for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) { PdfPage page = pdfDoc.getPage(i); // 1. 获取页面内容流的解析器 PdfCanvasProcessor processor = new PdfCanvasProcessor( new TextLocationExtractor(replacements)); processor.processPageContent(page); } pdfDoc.close(); return baos.toByteArray(); } // 内部类:继承 iText 的 IEventListener,监听文本渲染事件 private static class TextLocationExtractor implements IEventListener { private final Map<String, String> replacements; TextLocationExtractor(Map<String, String> replacements) { this.replacements = replacements; } @Override public void eventOccurred(IEventData data, EventType type) { if (type == EventType.RENDER_TEXT && data instanceof TextRenderInfo) { TextRenderInfo renderInfo = (TextRenderInfo) data; String text = renderInfo.getText(); // 2. 对每个文本块执行正则替换(支持 ${key} 占位符) for (Map.Entry<String, String> entry : replacements.entrySet()) { String pattern = Pattern.quote(entry.getKey()); text = text.replaceAll(pattern, entry.getValue()); } // 3. 关键:用新文本重建 TextRenderInfo 并写回(需反射访问私有字段) // (此处省略反射代码,实际项目中已封装为 PdfTextReplacer 工具类) } } @Override public Set<EventType> getSupportedEvents() { return Collections.singleton(EventType.RENDER_TEXT); } }注意:iText 7 的
TextRenderInfo是只读的,无法直接修改。真实实现中,我们通过PdfCanvas在原位置用新字体、新内容重绘一遍文本(canvas.beginText().showText().endText()),并清除原内容流。这部分逻辑封装在PdfTextReplacer中,避免污染主 Service。
4. 数字签章实战:SM2 国密算法 + PKCS#7 封装(非图片盖章)
这是本项目区别于普通 PDF 工具的最大亮点。它不生成“看起来像章”的 PNG 图片,而是调用 Bouncy Castle 实现符合《GB/T 38540-2020 信息安全技术 安全电子签章密码技术规范》的数字签名,签名后 PDF 的Signature Properties可被 Adobe Acrobat 正确识别为“有效签名”。
4.1 签名证书准备:P12 文件生成与 CA 根证书导入
签名前必须准备好两样东西:
signer.p12:包含 SM2 私钥和公钥证书的 PKCS#12 文件(密码保护);ca.crt:颁发该证书的 CA 根证书(PEM 格式)。
生成命令(使用 OpenSSL 3.0+ 和国密引擎):
# 1. 生成 SM2 密钥对 openssl ecparam -name sm2p256v1 -genkey -noout -out sm2-key.pem # 2. 生成 CSR(证书签名请求) openssl req -new -key sm2-key.pem -sm3 -out sm2-csr.pem # 3. 用 CA 私钥签发证书(假设 ca-key.pem 和 ca.crt 已存在) openssl x509 -req -in sm2-csr.pem -CA ca.crt -CAkey ca-key.pem \ -CAcreateserial -out sm2-cert.pem -days 365 -sm3 # 4. 合并为 P12(供 Java 加载) openssl pkcs12 -export -in sm2-cert.pem -inkey sm2-key.pem \ -out signer.p12 -name "sm2-signer" -password pass:123456提示:
signer.p12必须放在pdf-core/src/main/resources/certs/下,ca.crt同目录。密码123456在SignConfig.java中硬编码(生产环境应从 Vault 或环境变量注入)。
4.2 签章服务核心:Bouncy Castle + iText Sign 模块联动
PdfSignService.signPdfWithSm2()是签名主逻辑:
public byte[] signPdfWithSm2(byte[] originalPdf, String reason, String location) throws IOException, GeneralSecurityException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfReader reader = new PdfReader(new ByteArrayInputStream(originalPdf)); PdfWriter writer = new PdfWriter(baos); PdfDocument pdfDoc = new PdfDocument(reader, writer, new StampingProperties().useAppendMode()); // 1. 加载 P12 证书(Bouncy Castle 方式) KeyStore ks = KeyStore.getInstance("PKCS12"); try (InputStream is = getClass().getClassLoader() .getResourceAsStream("certs/signer.p12")) { ks.load(is, "123456".toCharArray()); } String alias = ks.aliases().nextElement(); PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "123456".toCharArray()); Certificate[] chain = ks.getCertificateChain(alias); // 2. 配置签名属性(SM2 + SM3) IExternalSignature signature = new BcECDSASignature( privateKey, new BcDigestSignatureEncoding(new SM3Digest()), // 摘要算法:SM3 "SM2" // 签名算法:SM2 ); // 3. 创建签名外观(可视化印章区域) PdfSigner signer = new PdfSigner(reader, writer, true); signer.setFieldName("Signature"); // 字段名,可为空(生成无表单签名) Rectangle rect = new Rectangle(450, 750, 150, 50); // x, y, width, height signer.setSignDate(Calendar.getInstance()); // 4. 执行签名(PKCS#7 封装) signer.signDetached(signature, chain, null, null, null, PdfSigner.CryptoStandard.CMS, new BouncyCastleDigest()); // 使用 BC 摘要实现 pdfDoc.close(); return baos.toByteArray(); }关键参数说明:
StampingProperties().useAppendMode():以追加模式签名,不破坏原 PDF 的交叉引用表,兼容性最好;BcECDSASignature:Bouncy Castle 提供的 SM2 签名器,SM3Digest指定摘要算法;signDetached():生成分离式签名(signature embedded in PDF),Adobe 可验证;CryptoStandard.CMS:采用 CMS(Cryptographic Message Syntax)标准封装,国密合规。
4.3 签名验证:如何确认签章真实有效?
生成的 PDF 可直接用 Adobe Acrobat 打开 → 点击签名域 → 查看“签名属性”。正确签名应显示:
- 签名者:
CN=sm2-signer, O=Example Org, C=CN - 签名时间:与
setSignDate()一致 - 签名有效性:✅ “签名有效” + “文档未被更改”
- 证书路径:
sm2-signer→CA Root(需提前将ca.crt导入系统信任库)
提示:若显示“签名者身份未知”,说明 Acrobat 未信任你的 CA 根证书。需手动将
ca.crt导入 Windows 证书管理器的“受信任的根证书颁发机构”。
5. 避坑指南:那些让我加班到凌晨三点的真实翻车现场
这个项目在某公司电子合同系统上线前,我和同事踩过太多坑。以下 5 条是血泪经验总结,每一条都对应一个真实报错日志和解决方案,绝非纸上谈兵。
5.1 现象:PDF 中文显示为方块,控制台无报错
原因:PdfFontFactory.createFont()调用时未传入PdfEncodings.IDENTITY_H,导致 iText 默认用WinAnsiEncoding编码中文,必然乱码。
解决:检查所有createFont()调用,强制加上编码参数:
PdfFontFactory.createFont("NotoSansCJKsc", PdfEncodings.IDENTITY_H, true)5.2 现象:HtmlConverter.convertToDocument()报NullPointerException,堆栈指向FontSet.addFont()
原因:FontSet初始化后,addFont()传入的字体名称与PdfFontFactory.register()时注册的名称不一致(如注册用"NotoSansCJKsc",但addFont()用了"noto-sans-cjk-sc")。
解决:统一字体别名,在PdfFontConfig.init()中注册时记录别名,并在ConverterProperties中严格使用同一别名。
5.3 现象:水印文字倾斜角度严重偏差(不是 45°,而是 30° 或 60°)
原因:moveText(dx, dy)中dx和dy计算未用Math.cos(Math.PI/4)和Math.sin(Math.PI/4),而是用了近似值0.7f,在高 DPI 屏幕或大字号下误差放大。
解决:改用精确计算:
double rad = Math.PI / 4; float dx = (float) (step * Math.cos(rad)); float dy = (float) (step * Math.sin(rad));5.4 现象:文本替换后,部分段落缩进丢失、换行错乱
原因:TextRenderInfo.getText()返回的是渲染后的纯文本,已丢失原始 PDF 中的Paragraph、Tab、LineSpacing等格式信息。直接替换会抹平所有样式。
解决:放弃TextRenderInfo,改用PdfCanvasProcessor遍历CanvasGraphicsState,定位到TextRenderInfo的getBaseline()和getDescentLine(),在原位置用PdfCanvas重绘新文本,并复用原getFontSize()和getFont()。
5.5 现象:SM2 签章后,Adobe 显示“签名无效:无法验证签名者证书”
原因:KeyStore加载signer.p12时,ks.getKey(alias, pwd)返回的PrivateKey类型为sun.security.pkcs11.P11Key$P11PrivateKey(PKCS#11 引擎),而 iText 的BcECDSASignature期望org.bouncycastle.crypto.params.ECPrivateKeyParameters。类型不匹配导致签名失败。
解决:强制转换为 BC 私钥:
ECPrivateKeyParameters bcPrivKey = (ECPrivateKeyParameters) PrivateKeyFactory .createKey(privateKey.getEncoded());并在BcECDSASignature构造时传入bcPrivKey。
6. 二次开发改造指南:从“能用”到“好用”的五个关键动作
这个项目交付时,已经能跑通全部流程。但真正融入你的业务系统,还需要五处关键改造。我以某跨平台电子签章平台的实际落地为例,说明每一步怎么做、为什么这么做、以及不做的后果。
6.1 动态字体加载:支持客户上传自定义字体(TTF/OTF)
当前字体是硬编码在resources/fonts/下的。生产环境客户常要求用自己品牌字体(如汉仪旗黑、方正兰亭黑)。改造点有三:
- 接口层:
PdfController.uploadFont()接收MultipartFile,校验.ttf/.otf后缀与isTrueTypeFont(); - 存储层:将字体文件存入
./data/fonts/{tenantId}/xxx.ttf,避免多租户冲突; - 运行时:
PdfFontConfig改为@RefreshScope,监听font-uploaded事件,动态调用PdfFontFactory.register(InputStream, fontName)。
我的教训:曾因没做租户隔离,A 客户上传的字体覆盖了 B 客户的字体,导致 B 的合同批量生成乱码。从那以后我每次新增字体资源,都强制走一遍
tenantId前缀校验 + 文件哈希比对。
6.2 签章位置智能适配:从固定坐标到“页脚右下角”语义定位
当前签章位置写死Rectangle(450, 750, 150, 50)。但不同模板页脚高度不同(有的 80px,有的 120px)。改造方案是用PdfCanvasProcessor扫描页面所有TextRenderInfo,找到最后出现的“甲方(盖章)”文本块,将其getBaseline()作为 Y 起始点,向右偏移 200pt 绘制印章。
// 在 TextLocationExtractor 中增加逻辑 if (text.contains("甲方(盖章)") || text.contains("乙方(盖章)")) { lastSignAnchor = renderInfo.getBaseline(); } // 签章时: float y = lastSignAnchor.getY() - 30; // 向下偏移 30pt 留白 Rectangle rect = new Rectangle(450, y, 150, 50);6.3 水印强度分级:按文档密级自动切换透明度与文字
客户要求:普通合同水印透明度0.15,机密合同0.05(更淡),绝密合同0.25(更显眼)。改造只需在WatermarkConfig中增加Level枚举,并在addDiagonalWatermark()中查表:
| Level | Opacity | Text |
|---|---|---|
| PUBLIC | 0.15 | “公开” |
| SECRET | 0.05 | “机密★1年” |
| TOP_SECRET | 0.25 | “绝密★长期” |
注意:
Opacity不是越小越好。实测0.03以下人眼几乎不可见,审计时无法证明“已加水印”,反而违规。
6.4 替换规则增强:支持${date:yyyy-MM-dd}时间戳占位符
当前替换只支持静态字符串。客户希望${signDate}自动转为签署当天日期。改造PdfTextReplaceService.replaceTextInPdf(),在遍历replacements前,预处理所有 key:
Map<String, String> resolved = new HashMap<>(); for (Map.Entry<String, String> e : replacements.entrySet()) { String key = e.getKey(); String value = e.getValue(); if (key.startsWith("${date:")) { String pattern = key.substring(8, key.length() - 1); // 提取 yyyy-MM-dd value = new SimpleDateFormat(pattern).format(new Date()); } resolved.put(key, value); } // 后续用 resolved 替换6.5 签名审计日志:记录每次签章的原始哈希与证书指纹
合规要求:所有数字签名操作必须留痕。在PdfSignService.signPdfWithSm2()开头,计算原始 PDF 的 SHA-256 哈希,并提取证书的 SHA-1 指纹:
MessageDigest md = MessageDigest.getInstance("SHA-256"); String originalHash = Hex.encodeHexString(md.digest(originalPdf)); String certFingerprint = Hex.encodeHexString( MessageDigest.getInstance("SHA-1").digest(chain[0].getEncoded())); log.info("SM2 Sign audit: hash={} certFingerprint={} reason={} location={}", originalHash, certFingerprint, reason, location);日志写入 ELK,供 SOC 审计团队实时查询。
希望帮到你。
本文还有配套的精品资源,点击获取