简介:这份资源面向Java后端开发者与电子合同业务研发人员,提供基于Spring Boot框架生成电子签章的完整实现参考,重点解决PDF合同签章环节中数字签名与图形印章叠加的技术问题。压缩包共72KB,以Java源码与Maven配置为主,pom.xml集中声明Spring Boot Starter Web、PDF处理及加密相关依赖,src/main/java下包含主程序类与电子签章服务逻辑,src/main/resources用于存放证书、密钥与印章图片等素材,target为编译输出目录。已有1250人学习下载,说明其在同类需求中具备一定参考价值。读者可从中了解JCA/JCE与Bouncy Castle在非对称加密签名中的应用方式,以及借助iText或PDFBox在PDF指定位置插入签章图像的实现思路,并参考HTTP接口接收请求、调用后端服务完成签章的整体流程,为自建电子合同签章模块提供可复用的代码骨架与目录组织范例。
1. 电子签章不是“贴张图”:从一份合同盖章需求说起
合同审批走完,业务方丢过来一句话:“把章盖上,生成 PDF 发出去。”听起来像调个图片合成接口,真动手才发现坑在后面:章要能验证、位置不能压到签字栏、多页合同每页都要盖骑缝、盖完的 PDF 不能被二次编辑、验签时还得说清是谁在什么时候盖的。这套东西在电子合同系统里叫电子签章,Java 侧落地通常绕不开 Spring Boot 做服务编排、PDFBox 或 iText 做文档处理、BouncyCastle 做密码学运算。它适合两类人:正在做电子合同/审批流毕设或课设的学生,需要一套能跑通、能讲清原理的结构;以及要给内部系统补签章能力的开发者,想要一份可直接改的方案骨架。下面按需求、设计、实现、避坑、验收的顺序拆开讲,重点放在“章怎么盖得合法、可验证、不出错”上。
2. 需求与选型:签章服务到底要交付什么
2.1 先把功能边界划清楚
电子签章服务不是“图片水印服务”,它的交付物是一份带密码学证据的 PDF。我一般会把需求拆成四层,缺一层后面就要返工。
第一层是文档层:支持上传 PDF、按坐标定位盖章、多页合同逐页盖、骑缝章沿页边切分。第二层是签章层:印章图片叠加、数字签名写入、签名域(Signature Field)预留。第三层是证据层:签署人标识、签署时间戳、文档哈希、证书序列号,这些要能落库,验签时能对上。第四层是接口层:盖章、验签、撤销、下载四个动作,配合审批流的状态机。
很多课设翻车就翻在只做了第一层,交上去的 PDF 用阅读器打开能看到章,但“签名”面板是空的,答辩老师一句“这章能证明是谁盖的吗”就答不上来。所以功能边界里必须明确:可见印章只是表象,不可见的数字签名才是证据。
2.2 技术栈为什么是这套组合
选型上,Spring Boot 负责服务层没争议,争议在 PDF 和密码学两块。PDF 处理常见三选一:PDFBox 纯 Java、无外部依赖,适合学生环境和轻量服务;iText 功能全、API 顺手,但要注意许可证,商用需评估;OpenPDF 是 iText 的老分支,LGPL 相对宽松。我的建议是毕设和内部系统用 PDFBox,理由不是它最强,而是它“黑匣子”少,出问题能顺着源码查,答辩时也讲得清坐标变换。
密码学部分用 BouncyCastle,它提供 CMS/PKCS#7 签名结构和证书链处理,配合 JDK 自带的 KeyStore 管理私钥。时间戳如果要做可信时间,通常对接第三方 TSA 服务,课设阶段可以先用本地时间加签名,但要在文档里注明这是简化处理,别把简化说成完整方案。
| 组件 | 选型 | 职责 | 备注 |
|---|---|---|---|
| 服务框架 | Spring Boot | 接口编排、事务、文件存储 | 版本按团队基线走 |
| PDF 处理 | Apache PDFBox | 加载、坐标定位、图层叠加、保存 | 纯 Java,无外部进程 |
| 密码学 | BouncyCastle | CMS 签名、证书解析 | 与 JDK KeyStore 配合 |
| 存储 | 本地磁盘或对象存储 | 原文件、签章后文件、证据记录 | 文件名用哈希,避免覆盖 |
| 元数据 | 关系型数据库 | 签署记录、哈希、状态 | 与文件分离,便于审计 |
2.3 非功能需求别漏掉
功能之外有三条硬约束经常被忽略。其一是幂等:同一份合同重复调用盖章接口,不能生成两份证据不同的文件,通常用“合同ID + 签署人 + 文档哈希”做唯一键。其二是不可篡改:签章后的文件一旦落盘,后续任何修改都要走新版本,不能原地覆盖,否则验签必然失败。其三是可追溯:每次盖章要记录操作人、时间、IP、证书序列号,出纠纷时这是唯一的后悔药。这三条写进需求文档,后面设计和验收才有依据。
3. 总体架构设计:四层结构与签章主流程
3.1 分层结构与模块职责
整体按四层切:接入层、业务层、签章引擎层、基础层。接入层暴露 REST 接口,做参数校验和鉴权;业务层管合同状态机和签署记录;签章引擎层是核心,封装 PDF 加载、坐标计算、图像叠加、数字签名四步;基础层管文件存储、证书库、数据库。
这样切的好处是签章引擎可以独立测试——给它一个 PDF 和一组参数,输出一个带签名的 PDF,不依赖 Spring 容器。学生做课设时,把引擎层单独写个 main 方法跑通,再套服务层,调试成本会低很多。模块之间通过接口通信,比如SignEngine.sign(byte[] pdf, SignRequest req),业务层不关心底层用的是 PDFBox 还是 iText,将来换实现不影响上层。
3.2 签章主流程的六个步骤
一次完整的盖章请求走六步:校验合同状态是否允许签署、加载原始 PDF、计算印章落点坐标、叠加可见印章图像、写入数字签名、保存文件并落库证据。顺序不能乱,尤其是“先叠图后签名”——数字签名保护的是最终字节流,如果先签名再改图,签名立刻失效。
坐标计算是最容易出错的一步。PDF 的坐标系原点在左下角,而前端传的通常是左上角原点,中间要做一次y' = pageHeight - y - imageHeight的翻转。多页合同还要判断印章是否跨页,骑缝章则要按页数把一张章图纵向切分,每页贴一条。这些逻辑放在引擎层的一个CoordinateResolver里,单独写单元测试,比在业务代码里散着算靠谱得多。
3.3 数据模型与状态机
数据库至少三张表:合同表、签署记录表、印章证书表。合同表存文件路径、哈希、当前状态;签署记录表存每次签署的签署人、时间、坐标、签名值摘要;印章证书表存证书别名、序列号、有效期。状态机建议四态:待签署、签署中、已签署、已撤销,状态流转必须带条件更新,防止并发下同一合同被盖两次。
-- 签署记录表:一次盖章一条,作为验签和审计的依据 CREATE TABLE sign_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_id BIGINT NOT NULL COMMENT '合同ID', signer_id VARCHAR(64) NOT NULL COMMENT '签署人标识', doc_hash CHAR(64) NOT NULL COMMENT '盖章前文档SHA-256', signed_hash CHAR(64) NOT NULL COMMENT '盖章后文档SHA-256', cert_serial VARCHAR(128) NOT NULL COMMENT '证书序列号', sign_time DATETIME NOT NULL COMMENT '签署时间', page_no INT NOT NULL COMMENT '盖章页码,骑缝为0', pos_x INT NOT NULL COMMENT '落点X', pos_y INT NOT NULL COMMENT '落点Y', status TINYINT NOT NULL DEFAULT 1 COMMENT '1有效 0已撤销', UNIQUE KEY uk_contract_signer (contract_id, signer_id, doc_hash) );这段建表语句的关键在最后那个唯一索引:contract_id + signer_id + doc_hash三者组合唯一,天然实现幂等——同一份文档同一个人重复提交,数据库直接拒绝,业务层捕获后返回已有记录即可。doc_hash和signed_hash分开存,是为了验签时能证明“签名针对的是哪一版字节流”。page_no为 0 表示骑缝章,这个约定要写进接口文档,否则前端传错页码,章就盖到空白页上去了。
4. 核心实现:PDF 坐标定位、印章叠加与数字签名
4.1 印章图像叠加的实现
可见印章的叠加本质是往 PDF 页面内容流里插入一张图片。PDFBox 的做法是加载页面、创建内容流、用PDImageXObject嵌入图片、再用PDPageContentStream在指定坐标绘制。这里有个血泪经验:内容流必须用追加模式打开(AppendMode.APPEND),否则会把页面原有内容清掉,合同正文直接消失。
// 在指定页面坐标叠加印章图片,坐标原点为页面左下角 public void stampImage(PDDocument doc, int pageIndex, byte[] sealBytes, float x, float y, float width, float height) throws IOException { PDPage page = doc.getPage(pageIndex); // 关键:APPEND 模式追加内容,避免覆盖原有正文 try (PDPageContentStream cs = new PDPageContentStream( doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) { PDImageXObject img = PDImageXObject.createFromByteArray(doc, sealBytes, "seal"); // 透明度通过 ExtGState 控制,避免章图完全遮住文字 PDExtendedGraphicsState gs = new PDExtendedGraphicsState(); gs.setNonStrokingAlphaConstant(0.85f); cs.setGraphicsStateParameters(gs); cs.drawImage(img, x, y, width, height); } }参数说明:pageIndex从 0 开始,和前端页码差 1,转换在业务层做;x、y是左下角原点坐标,前端传左上角坐标时要在调用前翻转;width、height按印章实际尺寸缩放,通常控制在 120×120 到 160×160 点之间,太大压正文、太小看不清。透明度设 0.85 是折中,既能看清章,又不至于把下面的签字文字完全盖死。这段代码只负责“看得见的章”,它不产生任何密码学证据,所以必须和下一节的签名配合使用。
4.2 数字签名的写入
数字签名是这套方案的核心。原理上,签名不是对“整个文件”做一次哈希,而是对文档的字节范围做摘要,再把摘要、证书、签名算法封装成 CMS 结构写进签名域。PDFBox 提供了SignatureInterface回调,在回调里用私钥对内容签名。
// 使用 KeyStore 中的私钥对文档做 CMS 签名 public byte[] signContent(InputStream content, PrivateKey key, X509Certificate cert) throws Exception { CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); // 摘要算法用 SHA-256,签名算法随密钥类型走(RSA 或 ECDSA) ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA") .setProvider(new BouncyCastleProvider()) .build(key); gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( new JcaDigestCalculatorProviderBuilder().build()) .build(signer, cert)); gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); CMSSignedData signed = gen.generate(new CMSProcessableInputStream(content), false); return signed.getEncoded(); }这段代码里false表示不封装原始内容(detached 签名),PDF 场景必须这样,因为原文已经在文件里,签名只存摘要和证书。SHA256withRSA要和证书公钥算法匹配,如果证书是 ECC 的,这里要换成SHA256withECDSA,否则验签必失败。私钥来源通常是 PKCS#12 格式的 KeyStore,加载时用KeyStore.getInstance("PKCS12"),密码从配置中心或环境变量取,绝不能硬编码在代码里——这是安全审计的必查项。
4.3 签名域与可见签名的绑定
只写 CMS 签名,阅读器里“签名”面板会显示,但页面上看不到章。要让两者绑定,需要在 PDF 里创建一个签名域(widget annotation),把可见印章图像作为该域的外观(appearance),再把签名值写进这个域。PDFBox 的PDVisibleSignDesigner和PDVisibleSigProperties就是干这个的,它把坐标、图片、签名域三者串起来。
实现顺序是:先用PDVisibleSignDesigner设定页码、坐标、印章图、缩放,再用PDVisibleSigProperties设定签名者名称、地点、原因,最后调用signPDF完成。注意签名域的矩形区域要和印章图像尺寸一致,否则会出现“章在左边、签名框在右边”的错位。多页合同每页都要盖时,循环创建多个签名域,每个域对应一页,签名值可以复用同一份 CMS 结构,但域必须独立。
4.4 验签接口的实现
验签是答辩时最能体现深度的一环。做法是加载已签名的 PDF,遍历所有签名域,取出 CMS 结构,用证书链验证签名值,再比对文档当前哈希与签名时记录的哈希。PDFBox 的PDSignature能拿到签名字节和证书,CMSSignedData负责密码学验证。
验签结果要分三级返回:密码学有效(签名值对得上)、证书可信(证书链能追到受信根)、文档未篡改(当前字节流哈希与签名覆盖范围一致)。三级全过才算“验签通过”。很多实现只做第一级,结果文档被改了一个空格也报“有效”,这在真实场景里是事故。验签接口返回结构建议包含valid、certTrusted、modified、signer、signTime五个字段,前端据此展示不同提示。
5. 避坑与排查:签章服务最容易翻车的五个点
5.1 盖章后 PDF 打不开或正文丢失
现象:盖章成功,下载的 PDF 用阅读器打开报“文件损坏”,或者正文整页消失只剩印章。原因几乎都是内容流模式用错——用覆盖模式打开PDPageContentStream,把原有页面内容清空了;或者保存时doc.save()写到了同一个输入流上,造成读写冲突。解决:内容流一律用AppendMode.APPEND;保存到新的ByteArrayOutputStream或临时文件,确认写入完成后再替换原文件;保存后立即用 PDFBox 重新加载一次做自检,加载失败就回滚。
5.2 验签通过但阅读器提示“签名无效”
现象:自己写的验签接口返回有效,但用常见 PDF 阅读器打开,签名面板显示黄色警告。原因通常是签名覆盖范围不对——先叠了印章图再签名,签名只覆盖了部分字节;或者签名后 PDFBox 又做了一次增量保存,追加了未签名的字节。解决:严格保证“所有可见修改完成后,最后一次操作才是签名”,签名后不再对文档做任何写操作;如果业务需要加页码或水印,全部前置到签名之前完成。
5.3 坐标偏移,章盖到页边或压住签字栏
现象:前端预览位置正确,实际盖章偏了几十点,或者多页合同只有第一页位置对。原因:PDF 页面可能有旋转(/Rotate90/180),也可能有裁剪框(CropBox)与媒体框(MediaBox)不一致,直接用 MediaBox 高度算翻转就会偏。解决:取页面实际CropBox计算,并判断getRotation(),旋转 90 度时 x、y 要互换;把坐标换算封装成独立方法,用几组已知页面做单元测试,别靠肉眼调。
5.4 并发下同一合同被盖两次
现象:审批流回调重试,或者用户连点两次,同一份合同出现两条签署记录,甚至两个不同的签名。原因:接口没做幂等,状态检查与写入之间存在竞态窗口。解决:数据库唯一索引兜底(见 3.3 的uk_contract_signer),业务层用条件更新UPDATE ... WHERE status = '待签署',影响行数为 0 就说明已被处理,直接返回已有结果;盖章动作本身放在事务内,文件写入用“临时文件 + 原子重命名”。
5.5 证书过期或私钥泄露无人察觉
现象:系统跑了半年,某天验签突然大面积失败,排查发现签名证书已过期;或者私钥文件权限过宽,被其他进程读取。原因:证书有效期没有监控,私钥管理随意。解决:启动时和每日定时任务检查证书剩余有效期,低于阈值告警;私钥只从受控的 KeyStore 加载,文件权限收紧到仅服务账户可读;签署记录里存证书序列号,出问题时能快速定位是哪张证书签的,换证书后旧文件仍可验签。
6. 验收与答辩:怎么证明这套签章方案是站得住的
验收这件事,我一般不看“功能演示能不能跑”,而是看“证据链能不能闭环”。具体做法是准备一份验收清单,逐项跑通并留痕。第一项,功能验收:上传一份三页 PDF,分别做单页盖章、逐页盖章、骑缝章,检查每页印章位置、透明度、是否压正文,用阅读器打开确认正文完整。第二项,密码学验收:用验签接口和至少一款常见阅读器分别验证,两者结论必须一致;故意改动签名后文档的一个字节,验签必须报“已篡改”。第三项,幂等验收:同一请求连发三次,数据库只能有一条有效记录,返回的文件哈希三次一致。第四项,异常验收:传非 PDF 文件、传超大文件、传损坏 PDF,接口要返回明确错误码而不是 500 堆栈。
答辩时最容易被追问的三个问题,提前准备好答案。其一,“你的章和一张图片水印有什么区别?”答:水印只改像素,签章写入 CMS 签名,绑定证书和文档哈希,可验证签署人和完整性。其二,“私钥放哪,泄露了怎么办?”答:KeyStore 受控加载、权限收紧、证书序列号入库、支持换证后旧文件仍可验签。其三,“多页合同怎么保证每页都没被换?”答:逐页签名域独立,或对整份文档签名后校验页数哈希,两种方案各有取舍,说清你选哪种、为什么。
下面这张表可以直接当验收记录模板用,每跑完一项打勾并附截图或日志路径,答辩时拿出来比口头描述有说服力。
| 验收项 | 操作 | 通过标准 | 留痕 |
|---|---|---|---|
| 单页盖章 | 上传单页 PDF 盖章 | 章位置正确、正文完整 | 文件 + 截图 |
| 逐页盖章 | 三页合同逐页盖 | 每页均有章且不重叠 | 文件 + 页码记录 |
| 骑缝章 | 多页合同盖骑缝 | 章沿页边切分、拼合完整 | 文件 + 拼合截图 |
| 验签一致 | 接口验签 + 阅读器验签 | 两者结论一致 | 双方结果截图 |
| 篡改检测 | 改动签名后文件一字节 | 验签报篡改 | 验签返回体 |
| 幂等 | 同一请求连发三次 | 仅一条有效记录 | 数据库查询结果 |
| 异常输入 | 非 PDF / 损坏 PDF | 返回明确错误码 | 接口响应日志 |
最后说个习惯。早些年我做第一个签章 Demo,功能演示全过,结果验收时对方拿阅读器一开,签名面板一片黄,当场尴尬。从那以后我每次交付签章相关的东西,都强制走一遍“阅读器 + 接口”双验签,并且把篡改测试当成固定动作,不跑完不提交。这套流程看着笨,但能挡住绝大多数“演示能过、验收翻车”的情况。希望帮到你。
本文还有配套的精品资源,点击获取