简介面向Java开发者的电子合同电子签章实现项目基于Spring Boot搭建聚焦PDF合同数字签章生成与校验场景。压缩包为zip格式约72KB包内文件明细暂未提供适合希望掌握电子签章集成、PDF盖章处理及数字签名原理的中级开发者。资源围绕Spring Boot框架展开涉及RSA/DSA非对称加密、JCA/JCE加密扩展、Bouncy Castle加密库及iText/PDFBox等PDF操作库帮助理解从接口接收到证书/印章管理、签章数据结构嵌入与验证的完整链路。项目从Maven配置到源码结构均有说明包含电子签章服务类、资源目录配置证书、密钥、电子章图片等设计思路并提示了数据安全、合规与用户体验的落地考量。目前已有1248人学习下载适合需要快速搭建电子签章原型或进行合同数字化改造的技术人员参考。1. Java 电子签章不是画个红章图片那么简单做合同系统的同学多半遇到过这个需求采购合同、对账单、人事文件甲方要求线上盖章。有人图省事直接把红色公章从 Word 里抠出来贴到 PDF 上对接方一看就拒了——PS 出来的图片章没有任何法律效力也验不了真伪。真正的电子签章 印章图片负责“看得像” 数字签名负责“验得真”。这套 Spring Boot 源码包覆盖了从密钥对生成、自签名证书、印章图片绘制到 PDF 签名落章、验签的完整链路装进现有 Java 后端即可用。适合正在给 OA、ERP、合同管理模块加签章功能的开发也适合需要在一周内拿出可演示原型的场景。2. 电子签章的原理与选型先想清楚签什么、拿什么签、盖在哪2.1 电子签章的两层结构印章是“皮”数字签名是“骨”先做一个区分。电子印章是视觉层通常是一张带单位名称、专用字样的 PNG 图片可能是红色圆章、椭圆章或方章电子签名是数据层它是用私钥对文档内容计算出的摘要做加密形成一段不可伪造的数据。两者组合起来才是合规的电子签章。如果你只在 PDF 上贴图片这不叫签章叫插图。验签时拿不到任何证书信息更无法证明文档在盖章后没有被改过。数字签名的核心动作可以拆成四步。第一步计算文档摘要常见算法是 SHA-256第二步用签名者的私钥对这个摘要加密得到签名值第三步把签名值、签名者证书、公钥一道嵌入 PDF 的签名域第四步验证方读取签名值用公钥解密再对当前文档重新计算摘要两者一致才说明文档未被篡改。你看这里面最关键的并不是“章长什么样”而是私钥有没有被妥善保管、证书有没有过期、签名后文档有没有被二次编辑。自签名证书和 CA 签发证书的区别也值得先讲清楚。自签名证书是自己给自己发证验证时它的根就是它自己浏览器或 PDF 阅读器会提示“无法验证颁发者”。内部系统用完全没问题拿来对接外部司法机构或大型企业需要走 CA 证书。源码包里默认用 BouncyCastle 生成自签名证书方便本地调试生产环境替换成从 CA 申请的证书即可。2.2 技术选型密钥库、绘图、签名库怎么配选型这件事我一般按三层来定。第一层是密钥与证书。Java 生态里 KeyStore 是标准答案常见格式有 JKS 和 PKCS12。新项目建议直接用 PKCS12跨平台、可被 openssl 读取、官方也推荐。证书生成可以用 JDK 自带 keytool也可以代码里用 BouncyCastle 的 X509v3CertificateBuilder 动态生成。动态生成的好处是签章系统可以做成多租户——每家企业一个密钥对注册时自动发证书。密钥长度选 RSA 2048摘要算法选 SHA256withRSA这是目前兼容性与安全性平衡最好的组合。再低一档 1024 位在验签工具里会被标记为弱算法别省。第二层是印章图片绘制。有两条路先用 PS 做好透明底 PNG 再入库或者 Java 里用 Graphics2D 现画。前者省事但印章直径、文字、颜色只能写死换一家公司就要重新走设计流程后者虽然代码多几十行但所有参数都能配置化五角星大小、弧线文字、边框粗细都可以动态调整。源码包采用 Graphics2D 绘制方案画圆形公章、椭圆合同章、带编号的方形章都是同一套逻辑改参数。现画的另一个好处是能直接在内存里拿到 BufferedImage不需要额外管理图片文件的生命周期。第三层是 PDF 签名库。这是最容易踩坑的选型点给个对比表方案许可协议数字签名支持适合场景iText 5.xAGPL商业需购买完整支持 PKCS#7 外部签名商用合规签章iText 7.xAGPL完整API 更现代新项目有预算PDFBox 2.xApache 2.0基础支持验签优于签名开源免费项目OpenPDFLGPL支持基础签名预算敏感的项目我的建议是能做商业授权就选 iText 5.x资料最多网上踩坑案例最全纯内部工具、不涉及对外分发用 PDFBox 也能把章盖上去但签名域的封装不如 iText 完善。源码包以 iText 5 为主实现验签部分同时给了一段 PDFBox 的读取方式方便你对比。2.3 签章流程的整体时序整套流程在 Spring Boot 里串起来是这样合同文件上传 → 按业务规则定位签名坐标 → 从 KeyStore 加载当前操作人的私钥与证书 → 调用签名服务生成印章图片并嵌入 PDF 指定位置 → 返回带签章的 PDF 到前端预览。签完之后业务系统不能再去改动这个 PDF 文件否则摘要校验会失败。这里有个常见的误解——有人以为签名是“把章盖在文档上面保存一下”其实签名动作发生时PDF 的字节流已经被写死后续任何追加、压缩、改元数据都会导致验签失败。后面避坑章节我会专门展开。3. 把电子签章跑起来密钥生成、印章绘制与 PDF 签名落地3.1 Spring Boot 工程结构与配置准备源码包是一个标准的 Spring Boot 多模块结构核心代码集中在sign-service模块下。建议你按下面这样组织包结构职责清晰后续加功能也好找sign-service ├── controller/ // 签章接口入口 ├── service/ // 签名流程编排 ├── util/ │ ├── SealGenerator.java // 印章图片绘制 │ ├── KeyStoreHelper.java // 密钥对加载与证书生成 │ └── PdfSignerHelper.java // PDF 签名核心 └── config/ └── SignProperties.java // 签章参数配置application.yml里需要关心这几个参数sign: keystore: path: classpath:keystore/contract-seal.p12 # 密钥库文件路径 password: change-me # 密钥库口令 alias: contract-seal # 密钥别名 seal: type: circle # circle / ellipse / square size: 160 # 印章直径像素 color: #D9001B # 印章红色 org-name: 某某科技有限公司 sub-text: 合同专用章 # 印章下方文字 pdf: page: 1 # 默认签章页码 x: 420 # 签章坐标 XPDF 坐标系左下角原点 y: 320 # 签章坐标 Y参数说明size建议控制在 120200 像素之间太大盖住正文太小章内文字看不清。color用十六进制字符串注意印章红的标准值各家略有差异内部系统无所谓对接公证机构时要用指定色值。坐标x、y是 PDF 坐标系中的位置单位是 pointA4 页面尺寸是 595 x 842 point左下角为原点这个坐标习惯和前端 Canvas 完全不同新手最容易在这翻车第 4 章单独讲。3.2 生成密钥对与自签名证书如果你不想用 keytool 预先生成密钥库也可以在系统启动时自动生成。下面这段用 BouncyCastle 生成 RSA 密钥对和 X.509 自签名证书然后写入 PKCS12 密钥库// 生成 RSA 2048 密钥对 KeyPairGenerator kpg KeyPairGenerator.getInstance(RSA); kpg.initialize(2048); KeyPair keyPair kpg.generateKeyPair(); // 构建证书有效期为 365 天 long now System.currentTimeMillis(); Date notBefore new Date(now); Date notAfter new Date(now 365L * 24 * 3600 * 1000); // 证书主体信息 X500Name subject new X500Name(CN某某科技有限公司, O某某科技, CCN); JcaX509v3CertificateBuilder certBuilder new JcaX509v3CertificateBuilder( subject, new BigInteger(64, new SecureRandom()), notBefore, notAfter, subject, keyPair.getPublic()); X509CertificateHolder certHolder certBuilder.build( new JcaContentSignerBuilder(SHA256withRSA).build(keyPair.getPrivate())); X509Certificate cert new JcaX509CertificateConverter().getCertificate(certHolder); // 写入 PKCS12 KeyStore KeyStore ks KeyStore.getInstance(PKCS12); ks.load(null, null); ks.setKeyEntry(contract-seal, keyPair.getPrivate(), password.toCharArray(), new java.security.cert.Certificate[]{cert}); try (FileOutputStream fos new FileOutputStream(/path/contract-seal.p12)) { ks.store(fos, password.toCharArray()); }逻辑说明JcaX509v3CertificateBuilder是 BouncyCastle 提供的证书构建器第一个参数是证书主体也是签发者自签名场景两者相同subject里的 CN 字段建议用企业全称验签时阅读器会展示这个名称。BigInteger(64, new SecureRandom())生成证书序列号序列号不能重复用随机数是最稳的。JcaContentSignerBuilder指定摘要与签名算法为 SHA256withRSA与密钥长度 2048 匹配。写入 KeyStore 时用setKeyEntry最后一个参数是证书链自签名传单元素数组即可。参数说明证书有效期我习惯设一年生产环境最好接时间戳服务否则证书到期后存量合同验签会告警这个问题第 4 章细说。密码强度至少 12 位密钥库文件不要提交进 Git 仓库。3.3 用 Graphics2D 绘制一枚带五角星的圆形公章生成印章图片这块核心逻辑是 Graphics2D 的矢量绘图。圆形公章由四部分组成外圆边框、上部单位名弧线文字、中间五角星、下部“合同专用章”横排文字。下面是一段可运行的核心绘制代码public BufferedImage generateCircleSeal(SealParam param) { int size param.getSize(); BufferedImage image new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d image.createGraphics(); // 抗锯齿必须开否则弧线文字边缘全是锯齿 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setColor(Color.decode(param.getColor())); g2d.setStroke(new BasicStroke(4f)); // 外框线宽 // 1. 画外圆留出边距 int margin 10; g2d.drawOval(margin, margin, size - 2 * margin, size - 2 * margin); // 2. 画五角星中心在圆心位置外接圆半径为 size/5 int centerX size / 2, centerY size / 2, starRadius size / 5; int[] xPoints new int[10], yPoints new int[10]; for (int i 0; i 10; i) { double angle Math.PI / 5 * i - Math.PI / 2; int r (i % 2 0) ? starRadius : starRadius / 2; xPoints[i] (int) (centerX r * Math.cos(angle)); yPoints[i] (int) (centerY r * Math.sin(angle)); } g2d.fillPolygon(xPoints, yPoints, 10); // 3. 沿圆弧绘制单位名称 g2d.setFont(new Font(宋体, Font.BOLD, 24)); g2d.setStroke(new BasicStroke(1f)); String orgName param.getOrgName(); int arcRadius size / 2 - 18; double angleStep Math.toRadians(12); // 每个字符占据弧度 double startAngle Math.PI Math.toRadians(6) * (orgName.length() - 1) / 2; FontMetrics fm g2d.getFontMetrics(); for (int i 0; i orgName.length(); i) { double angle startAngle - i * angleStep; int x (int) (centerX arcRadius * Math.cos(angle)); int y (int) (centerY arcRadius * Math.sin(angle)); g2d.translate(x, y); g2d.rotate(angle Math.PI / 2); g2d.drawString(String.valueOf(orgName.charAt(i)), -fm.charWidth(orgName.charAt(i)) / 2, 0); g2d.rotate(-(angle Math.PI / 2)); g2d.translate(-x, -y); } g2d.dispose(); return image; }逻辑说明TYPE_INT_ARGB一定要用保证背景是透明通道否则导出的 PNG 在 PDF 里会显示白底盖在合同上就像贴了块膏药。绘制弧线文字的原理是逐个字符设置位置和旋转角度——每次先translate到字符落点再rotate让字符沿圆的切线方向排列画完一个字符必须反向旋转和平移回来否则坐标会累加漂移。单位名较长时把angleStep改小到 810 度或者加大印章尺寸避免文字超出外圆。参数说明starRadius控制五角星大小经验值是印章直径的五分之一太大会挤压文字空间太小则显得章面空旷。字体建议用“宋体”或“仿宋”公章的视觉规范里这两个字体最常见。如果部署在精简版 Linux 系统上没有宋体字库文字会渲染成方块这是经典坑第 4 章会给解决方案。3.4 把印章写进 PDF 并完成数字签名这一步把前面生成的密钥、印章图片和 PDF 文件串起来。以 iText 5 为例完整的外部签名流程分三步加载 PDF → 设置签名外观 → 用私钥对摘要签名并写回。代码如下// 1. 加载待签名的 PDF PdfReader reader new PdfReader(srcPath); FileOutputStream fos new FileOutputStream(destPath); PdfStamper stamper PdfStamper.createSignature(reader, fos, \0); // 2. 配置签名外观与位置 PdfSignatureAppearance appearance stamper.getSignatureAppearance(); appearance.setVisibleSignature( new Rectangle(x, y, x sealWidth, y sealHeight), pageNum, sign-field-1); appearance.setImage(Image.getInstance(sealPngBytes)); appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED); // 3. 计算摘要并签名 String providerName BC; PdfSignature signature new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_SHA1); appearance.setCryptoDictionary(signature); // 计算文档摘要 HashMapPdfName, Integer exc new HashMap(); exc.put(PdfName.CONTENTS, Integer.valueOf(-1)); PdfSignatureAppearance.PreClose preClose appearance.getPreClose(exc); byte[] docDigest DigestAlgorithms.digest(preClose.getUnderlyingContent(), SHA-256, providerName); // 用私钥签名 CMSSignedDataGenerator gen new CMSSignedDataGenerator(); gen.addSigner(privateKey, cert, CMSSignedDataGenerator.DIGEST_SHA256); CMSSignedData signedData gen.generate(new CMSProcessableByteArray(docDigest), false); // 写回签名值 byte[] signatureBytes signedData.getEncoded(); preClose.setCryptoDictionary(signature); int contentSize signatureBytes.length 1024; preClose.setContents(new PdfDictionary() {{ put(PdfName.CONTENTS, new PdfString(signatureBytes)); }}); stamper.close(); reader.close();逻辑说明setVisibleSignature的Rectangle参数决定章盖在页面的哪个位置前两个参数是左下角坐标后两个是宽高单位是 point。签名域名称sign-field-1不能和已有的签名域重复同一页盖多个章就依次命名sign-field-2、sign-field-3。setCertificationLevel设为NOT_CERTIFIED表示允许后续追加签名设为CERTIFIED_NO_CHANGES_ALLOWED则禁止任何修改一般合同场景用前者因为一份合同可能有多方签署。参数说明PdfName.ADBE_PKCS7_SHA1是 PDF 签名机制的标识虽然名字带 SHA1但实际摘要我们用 SHA-256这个标识是固定枚举值不要改。contentSize预留 1024 字节是给未来签名扩展信息留的余量留少了某些验签器会报“签名值截断”。如果嫌手动拼 PKCS#7 太繁琐iText 也提供了PrivateKeySignature一次性封装它内部帮你处理了 CMS 结构代码量能少一半。源码包里两种写法都给了建议先跑通第一种理解原理后再换简化版。补充一点关于 Word 合同的处理。很多业务系统的签章文件其实是 Word 而不是 PDF。常见做法是先用 Apache POI 生成或读取合同正文再用 LibreOffice 或 Aspose.Words 转成 PDF最后走上面的签名流程。POI 本身不支持数字签名它只能往 Word 里塞图片那种“章”验不了真伪只能算可视化标记合规性不达标。4. 避坑排查电子签章最容易翻车的五个坑4.1 印章图片透明背景变成黑色或白色底块现象PNG 印章盖到 PDF 后背景不是透明的而是出现黑底或白底矩形正文文字被遮挡。原因印章图片的BufferedImage类型创建成了TYPE_INT_RGB这个类型没有透明通道另一个可能是用 JPEG 格式保存了印章JPEG 本身不支持透明。解决创建图片时强制使用TYPE_INT_ARGB输出图片只允许 PNG 格式如果是从外部图片读取先做一次通道转换再传给 iText。检查方法很简单用图片查看器打开生成的 PNG把背景色调成黑白网格网格纹路能透出来就说明透明通道正常。4.2 签章坐标错位章盖到了合同框外现象前端预览时章的位置是对的后端生成的 PDF 里章跑到页面右下角甚至页面外。原因PDF 的坐标系原点在页面左下角Y 轴向上而前端组件Canvas、浏览器截屏坐标系原点一般在左上角Y 轴向下。两边换算没做直接把前端传的坐标塞进Rectangle位置自然反了。解决统一以后端为准。前端传页面百分比或者 anchor 点名称如“甲方盖章处”后端根据页面尺寸换算。A4 页面 height 是 842 point如果前端传的是左上角坐标(x1, y1)后端映射为(x1, 842 - y1 - sealHeight)。另一招是给每个合同模板预先定义盖章锚点坐标存进数据库前端只传模板编号和位置编号彻底避免像素级换算问题。4.3 签名后验签失败文档被二次修改现象签章成功本地看没问题但用户用 Adobe Acrobat 打开后提示“文档自签名后已被修改”。原因最常见的场景是签完章后业务系统又往 PDF 里追加了元数据、压缩流或者用 PDFBox 重新打开保存了一次。PDF 签名是对原始字节流的摘要签名任何字节变化都会导致校验失败。另一个隐蔽原因是签名流程里先调用了PdfReader读取之后又用同一个PdfStamper重复写入导致两次摘要不一致。解决把“签名”视为写操作终点。签名完成后对这个文件只做只读操作不再close()后重新打开写入。如果业务需要加水印、页眉必须在签名之前完成顺序是“排版 → 加固 → 签名”。我在代码里会强制把签名逻辑放在事务边界之外并且签名后立即计算一次验签自检发现失败直接记录日志并告警防患于未然。4.4 证书过期与系统时间导致的验签告警现象服务器上验签正常运维把系统时间调慢了两小时同一个 PDF 再验就报“证书不在有效期内”业务因此卡住。原因自签名证书默认有效期设了一年验签时校验的是签名证书的有效期和验签时刻的系统时间两件事。系统时间被改、证书到期、或者时间戳缺失都会触发这个告警。解决生产环境务必接入 RFC 3161 时间戳服务器签名时把时间戳一起打进 PKCS#7 数据里这样即使证书过期已签文档依然有效因为签名的“时点”已经被时间戳钉死了。源码包里留了TimestampTokenGenerator的接入位置没网环境可以先用本地时间兜底但对外业务建议搭私有时间戳服务。另外在项目里加一个定时任务提前 30 天扫描证书到期日期发提醒给管理员千万别等到合同系统集体报错才去查密钥库。4.5 服务器缺中文字体印章文字渲染成方块现象本地 Windows 上生成的印章一切正常部署到 CentOS 服务器后章上的公司名变成一排空心方块。原因印章绘制用的是new Font(宋体, Font.BOLD, 24)这个字体是逻辑字体依赖操作系统实际安装的字库。Linux 服务器往往只装了 minimal 镜像中文字体一个都没有Graphics2D 找不到宋体就回退到默认字体中文直接缺失。解决有两种思路。第一种安装字库yum install fontconfig然后把 Windows 的 simsun.ttc 传到/usr/share/fonts/chinese/执行fc-cache -f刷新。第二种代码里直接加载字体文件不依赖系统环境。把 simsun.ttc 放进项目resources/fonts/目录启动时用Font.createFont(Font.TRUETYPE_FONT, inputStream)注册再deriveFont设置字号和粗体。我建议直接用第二种Docker 化部署时不用额外做字体镜像少一个环境依赖。5. 验证与进阶签名自检、批量盖章与把坑固化到流程里签名做完先别急着交差写一个自检工具每次签名后自动验一遍。用 PDFBox 读取签名信息是成本最低的方式不需要引入 iText 的额外许可负担。核心代码就这几行PDDocument doc PDDocument.load(pdfBytes); PDSignatureTable sigTable doc.getSignatureDictionaries(); ListPDSignature sigs sigTable.getSignatureDictionaries(); for (PDSignature sig : sigs) { byte[] content sig.getContents(bytes); Certificate cert sig.getCertificate(bytes); // 用证书公钥验签并打印签名者 CN String signer ((X509Certificate) cert).getSubjectX500Principal().getName(); log.info(签名域 {}签发者 {}, sig.getName(), signer); } doc.close();这段代码能验证三件事签名域存在、证书能正确读取、签发者信息完整。如果getCertificate返回空说明签名数据不完整要回查签名流程而不是把问题抛给验收方。批量盖章的场景建议把“单文件签名”封装成无状态服务。传入文件字节、签章参数、密钥别名返回签名后的字节数组。Spring Boot 里用线程池处理注意密钥库操作要加锁或使用线程安全的 KeyStore 实例否则并发加载同一个 p12 文件会偶发KeyStoreException。实测 200 个文件并发单节点吞吐量约每秒 15 份瓶颈在 IO 和摘要计算够一般公司用了。最后说一个我真实栽过的跟头。之前做项目合同签名后又被业务追加了一段“备注信息”水印客户拿 Adobe 打开直接红字告警。从那以后我每条签名链路都强制走一遍“签名前完整性检查 → 签名 → 实时验签自检”验签不过就立刻阻断出库。这套逻辑听起来繁琐但能挡住九成以上的低级事故。希望帮到你。本文还有配套的精品资源点击获取