简介:这份资料包面向需要在Java项目中落地OFD电子签章功能的开发者,围绕国标OFD文档格式,提供可运行的签章DEMO与配套使用说明,帮助理解数字证书、文档哈希、签章生成与验证等关键环节。压缩包为rar格式,共606个文件、约76.61MB,以462个java源码为主体,辅以31个ofd样例文档、21个xml配置、16个md说明及jpg、png等图片素材,另含esl、p12、der等证书与签章数据文件,便于对照调试。已有331人学习。读者可借助ofdrw开源库的解析、对象操作、签章服务与证书管理接口,结合示例代码完成文档读取、签章插入与有效性验证,并参考说明文档快速搭建环境、排查问题,形成可复用的OFD签章实现思路。
1. 从一份“签完章打不开”的OFD说起
去年帮某公司做档案系统对接,对方甩过来一个OFD文件,说“章盖好了,你验一下”。我用阅读器打开,章是有的,但换个工具再开,章没了;再换一个,文件直接报结构错误。翻回去看他们的实现,签章逻辑是拿PDF那套思路硬套上去的——把印章图片往页面上一贴,导出完事。这就是OFD签章最容易翻车的地方:OFD是容器化的版式文档,签章不是“贴图”,而是在指定坐标写入签章对象,并且要跟文档的签名描述文件、印章资源、保护条目对齐,任何一处对不上,别的阅读器就不认。
这份资源就是一个把这件事讲透的demo:它演示了怎么在一个OFD文档的指定位置完成电子签章,同时附带了OFD格式本身的使用说明——结构怎么组织、签章数据放在哪、验签要看哪些字段。适合两类人:一是手上有个业务系统要出带章OFD,被各家阅读器兼容性折磨过的后端开发;二是想搞清楚OFD签章数据到底长什么样、准备自己写签章模块的工程师。下面我按“这东西是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改成自己的”这个顺序拆。
2. 项目定位与技术选型:为什么不是“贴张图”那么简单
2.1 这个demo到底解决什么问题
先把边界划清楚。OFD(Open Fixed-layout Document)是国内版式文档标准,本质是个Zip包,里面按目录组织:文档根节点、页面内容、资源文件(字体、图片、印章)、签名描述文件(Signatures.xml)、以及每个签章对应的签名值文件。电子签章在OFD里不是画上去的图形,而是一个有结构的对象:它要引用印章图片资源、记录签章者信息、绑定被保护的内容摘要,最后把签名值单独存一份。
这个demo做的事,就是走完这条链路:加载一个已有OFD → 定位到目标页和目标坐标 → 构造签章对象并写入页面 → 生成签名描述和保护条目 → 输出新OFD。它同时带了一份OFD使用说明,把目录结构、关键XML节点、签章相关字段列了出来,相当于给你一张地图,免得你在Zip包里瞎翻。
适用场景很具体:合同、公文、票据这类需要“版式固定 + 可验签”的文档出具环节。不适合的场景也说一句——如果你只是想在图片上盖个章做展示,不需要验签,那用不着这套东西,直接合成图片更省事。
2.2 技术栈与依赖的取舍
从项目正文和关键词看,这是个偏Java生态的OFD签章demo(OFD在国内的落地实现,Java系工具链最成熟,常见做法是基于开源OFD解析库做二次封装)。核心依赖大致分三层:
| 层次 | 作用 | 常见选型 |
|---|---|---|
| 文档解析层 | 读写OFD容器、解析页面与资源 | 开源OFD解析库(如ofdrw一类) |
| 签章密码层 | 生成签名值、计算摘要、证书处理 | JDK自带JCA + BouncyCastle |
| 业务封装层 | 定位坐标、组装签章对象、输出文件 | 项目自身的签章服务类 |
为什么密码层不自己造轮子?因为签章涉及SM2/SM3或RSA/SHA256这类算法,自己实现摘要和签名编码,出错概率极高,而且验签方一换就崩。用成熟密码库,你只需要关心“签什么、签在哪、怎么组织进OFD结构”,这是这个demo的合理分工。
有一点要提前说清楚:签章用的证书和私钥,demo里通常是加载本地keystore或者PFX文件。真实业务里私钥不该出现在应用服务器上,常见做法是接签名服务器或者硬件密码设备,demo为了跑通简化了这一步,你二次开发时得把这块换掉。
2.3 目录结构长什么样
OFD文件解开后大致是这么个结构,理解它比看任何文档都快:
sample.ofd (本质是zip) ├── OFD.xml # 入口,指向文档根节点 ├── Doc_0/ │ ├── Document.xml # 文档主体,含页面列表 │ ├── DocumentRes.xml # 文档级资源(字体、图片) │ ├── Signs/ │ │ ├── Signatures.xml # 签名描述,记录有几个章、保护哪些条目 │ │ └── Sign_0/ │ │ └── SignedValue.dat # 签名值(二进制) │ └── Pages/ │ └── Page_0/ │ ├── Content.xml # 页面内容,签章对象就写在这里 │ └── Res.xml # 页面级资源,印章图片引用签章要动的地方就三处:Content.xml里加签章对象节点、Signatures.xml里加签名描述、Sign_0/SignedValue.dat写签名值。demo的代码基本就是围绕这三处展开的,后面拆代码时会一一对应。
3. 跑起来:环境准备与最小可运行路径
3.1 环境与依赖准备
先给结论:JDK 8 及以上、Maven 构建、一个能打开OFD的阅读器用来验证结果。依赖装好之后,项目一般能直接mvn clean package出可执行包。关键依赖大致是OFD解析库加密码库,pom里通常长这样(版本号以你实际拉到的为准,别照抄):
<dependencies> <!-- OFD 读写解析,负责容器和页面结构 --> <dependency> <groupId>org.ofdrw</groupId> <artifactId>ofdrw-full</artifactId> <version>2.x.x</version> </dependency> <!-- 密码算法支持,SM2/SM3 或 RSA 都靠它 --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> </dependencies>这里有个血泪经验:OFD解析库的版本和JDK版本有绑定关系,JDK 8 上跑高版本库可能报Unsupported class file major version,别急着怀疑代码,先看版本矩阵。另外BouncyCastle要注册Provider,否则SM2签名会抛NoSuchAlgorithmException,这一步demo里一般有,但你自己新建工程容易漏。
3.2 准备签章素材:证书、印章图、待签文档
跑demo前要备三样东西,缺一样都跑不通:
第一是证书和私钥。demo通常读一个PFX或JKS文件,里面带签名证书和私钥,密码写在配置里。你可以用keytool生成一个自签名证书先跑通流程,验签时用同一张证书的公钥验,别一上来就接正式CA证书,否则排查问题时你分不清是代码错还是证书链错。
第二是印章图片。OFD签章里的印章一般是一张PNG,带透明通道,尺寸按实际盖章大小来。注意图片别太大,几百KB的印章图塞进OFD会让文件体积暴涨,常见做法是压到几十KB。
第三是待签的OFD文档。如果你手上没有,可以用OFD阅读器或转换工具从Word/PDF转一份出来,确保它能正常打开再拿去签,源文件本身结构有问题的话,签完只会更乱。
3.3 执行签章并验证结果
最小运行路径一般是一个main方法或者一个测试类,核心调用就几步:加载文档、定位、签章、保存。跑的命令大致是:
# 编译打包 mvn clean package -DskipTests # 运行签章入口(类名以项目实际为准) java -cp target/classes:target/dependency/* com.example.ofd.SignDemo \ --in sample.ofd \ --out signed.ofd \ --cert sign.pfx \ --pwd 123456 \ --seal seal.png \ --page 0 \ --x 300 --y 400参数含义:--in/--out是输入输出路径,--cert/--pwd是证书文件和口令,--seal是印章图,--page是页码(从0开始,这点容易错),--x/--y是签章左下角坐标,单位是OFD的毫米坐标体系,不是像素。
跑完怎么验证?三步:用两三个不同厂商的OFD阅读器分别打开signed.ofd,看章是否显示、位置是否正确;在支持验签的阅读器里点验签,看是否提示“签名有效”;最后把文件当Zip解开,检查Signatures.xml里是否多了签名描述、Sign_0/SignedValue.dat是否存在且非空。三步都过,才算真的签成功,只看一个阅读器显示正常是不够的。
提示:坐标单位是毫米,且原点在页面左下角,跟你平时写前端用的左上角像素坐标是反的。第一次调位置基本都会偏,别怀疑库,先怀疑坐标系。
4. 核心代码拆解:一次签章从定位到落盘
4.1 加载文档与定位签章坐标
签章第一步是把OFD读进内存模型,然后找到目标页。这一步的关键不是“读”,而是“定位”——你得知道页面多大、坐标系怎么算,否则章会盖到页面外面去。
// 打开OFD文档,OFDReader 负责解析容器结构 try (OFDReader reader = new OFDReader(Paths.get("sample.ofd"))) { // 拿到文档对象,一个OFD可以有多份文档,通常取第一份 OFDDoc doc = reader.getOFDDoc(); // 取第0页,页码从0开始 OFDPage page = doc.getPage(0); // 页面尺寸,单位毫米 double pageW = page.getPageArea().getPhysicalBox().getWidth(); double pageH = page.getPageArea().getPhysicalBox().getHeight(); // 签章位置:左下角为原点,x向右,y向上 // 这里把章放在页面右下角往内缩 20mm 的位置 double sealW = 40.0; // 印章宽 40mm double sealH = 40.0; // 印章高 40mm double x = pageW - sealW - 20; double y = 20; // 后续用 x, y, sealW, sealH 构造签章对象 }逻辑说明:OFDReader是解析入口,getOFDDoc()拿文档,getPage(0)拿页。PhysicalBox是页面的物理尺寸,单位毫米。坐标计算这块,x = 页宽 - 章宽 - 边距、y = 边距,是把章放在右下角的标准写法。参数上,sealW/sealH要和你的印章图比例一致,否则章会被拉伸变形——这是很常见的翻车点,图片是正方形但代码里写了个长方形尺寸,盖出来就是扁的。
4.2 构造签章对象并写入页面
定位好了,接下来是把签章对象塞进页面内容。OFD里签章对象(Seal)是一个有类型的页面元素,它引用印章资源,并携带签章相关的元信息。
// 构造签章对象,绑定印章图片和位置 Seal seal = new Seal(); seal.setBoundary(new Boundary(x, y, sealW, sealH)); // 位置和大小 seal.setClapImage("seal.png"); // 印章图片资源名 seal.setSigner("某公司"); // 签章者名称 seal.setSignDate(new Date()); // 签章时间 // 把签章对象加入页面内容 page.getContent().add(seal); // 印章图片要作为页面资源注册进去,否则引用不到 page.getResources().addImage("seal.png", Paths.get("seal.png"));逻辑说明:Boundary是签章在页面上的矩形区域,四个参数分别是左下角x、y和宽高。setClapImage指定印章图片的资源名,这个名字必须和后面addImage注册的名字一致,不一致的话阅读器找不到图,章就是空白。setSigner和setSignDate是签章元信息,会写进签名描述里,验签时能看到。
参数上要特别注意Boundary的宽高和图片实际比例。另外,addImage注册资源这一步很多人会漏,以为设了图片路径就行,结果导出的OFD里章不显示——因为资源没进资源表,引用是悬空的。
4.3 生成签名描述与签名值
页面上的章只是“看起来有章”,真正让它成为“电子签章”的是签名描述和签名值。这一步要计算被保护内容的摘要,用私钥签名,然后把结果写进Signatures.xml和SignedValue.dat。
// 1. 计算被保护条目的摘要(这里保护整个文档) byte[] digest = SM3.hash(documentBytes); // 国密场景用SM3,RSA场景用SHA256 // 2. 用私钥对摘要签名 PrivateKey privateKey = loadPrivateKey("sign.pfx", "123456"); Signature signer = Signature.getInstance("SM3withSM2", "BC"); signer.initSign(privateKey); signer.update(digest); byte[] signedValue = signer.sign(); // 3. 写签名描述:记录签章者、时间、保护条目、签名方法 Signatures signatures = doc.getSignatures(); Sign sign = new Sign(); sign.setSigner("某公司"); sign.setSignDate(new Date()); sign.setSignatureMethod("SM3withSM2"); sign.setReferences(Collections.singletonList("Doc_0/Document.xml")); // 保护范围 signatures.add(sign); // 4. 签名值单独落盘 Files.write(Paths.get("Sign_0/SignedValue.dat"), signedValue);逻辑说明:摘要计算的对象是“被保护条目”,demo里通常保护整个Document.xml,意思是文档内容一改,验签就失败。Signature.getInstance("SM3withSM2", "BC")里的"BC"是BouncyCastle的Provider名,没注册就会抛异常。签名值不写进XML,而是单独存二进制文件,这是OFD的设计,别把它塞进Signatures.xml里。
参数上,setReferences决定保护范围,写错路径会导致验签时找不到被保护对象。setSignatureMethod要和实际用的算法一致,写SM3withSM2但实际用RSA签,验签方直接判无效。
4.4 保存输出与结构自检
最后一步是把内存模型写回文件,并做一次结构自检。
// 写出新OFD,OFDWriter 负责重新打包容器 try (OFDWriter writer = new OFDWriter(Paths.get("signed.ofd"))) { writer.write(doc); } // 自检:重新打开,确认签章对象和签名描述都在 try (OFDReader check = new OFDReader(Paths.get("signed.ofd"))) { boolean hasSeal = check.getOFDDoc().getPage(0) .getContent().getElements().stream() .anyMatch(e -> e instanceof Seal); boolean hasSign = !check.getOFDDoc().getSignatures().getSigns().isEmpty(); System.out.println("seal=" + hasSeal + ", sign=" + hasSign); }逻辑说明:OFDWriter会把内存模型重新打包成Zip容器,注意它可能覆盖原文件,输出路径别和输入相同。自检这段是我强烈建议保留的——签完不验,等于没签。hasSeal检查页面里有没有签章对象,hasSign检查签名描述有没有写进去,两个都为true才算结构完整。真正的密码学验签还得靠阅读器或验签接口,但结构自检能挡掉大部分低级错误。
5. 避坑与排查:签章不显示、验签失败的常见原因
5.1 章显示了但换个阅读器就没了
现象:在自己常用的阅读器里章正常显示,发给对方用另一个阅读器打开,章不见了。
原因:签章对象写进了Content.xml,但印章图片资源没注册进Res.xml,或者资源名和引用名不一致。有的阅读器容错强,找不到图也画个框;有的直接不渲染。
解决:解开OFD检查Res.xml里有没有对应的图片资源条目,名字是否和setClapImage完全一致(大小写敏感)。注册资源用addImage,别只设路径。
5.2 验签提示“文档已被篡改”
现象:章在,但点验签报摘要不匹配。
原因:签名时计算的摘要对象和验签时的不一致。常见是签完章之后又改了Document.xml(比如补了个字段),或者setReferences写的保护路径和实际被保护文件对不上。
解决:签章必须是文档修改的最后一步,签完不要再动内容。检查Signatures.xml里的References路径是否精确指向被保护条目,路径大小写和层级都要对。
5.3 坐标偏移,章盖到页面外
现象:章的位置和预期差一大截,甚至跑到页面外。
原因:坐标系搞反了。OFD原点在左下角、y轴向上,单位毫米;很多人按前端习惯用左上角原点、像素单位,算出来自然偏。
解决:统一用毫米,y坐标从页面底部往上算。调位置时先把章放在(0,0)看它出现在哪,确认坐标系方向后再算偏移,比盲调快得多。
5.4 SM2签名抛 NoSuchAlgorithmException
现象:运行到签名那行直接异常,提示找不到算法。
原因:BouncyCastle Provider没注册,或者JDK版本太低不支持对应算法。
解决:在程序入口加Security.addProvider(new BouncyCastleProvider()),确保只加一次。JDK 8 上跑国密建议用较新的BC版本,老版本对SM2支持不全。
5.5 输出文件体积异常增大
现象:签一个章,OFD从几百KB涨到几MB。
原因:印章图片没压缩,或者每次签章都把整份文档资源重复打包了一遍。
解决:印章图压到几十KB的PNG,带透明通道即可。检查写出逻辑是不是重复注册了资源,addImage同一个名字调多次可能产生冗余条目。
6. 二次开发:把demo改成能进生产的签章服务
demo跑通只是起点,真要接进业务系统,有几处必须改。我把改造路径和验证方法列在下面,都是踩过坑之后总结的。
第一处是密钥管理。demo从本地PFX读私钥,生产环境不能这么干。常见做法是抽一个SignService接口,本地实现用于测试,生产实现走签名服务器或密码设备,接口只暴露“传入摘要、返回签名值”。这样上层签章逻辑完全不用动,换实现就行。改造后一定要做一次对照验证:同一份文档,本地实现和远端实现签出来的章,在阅读器里都能验签通过。
第二处是坐标配置化。demo里坐标写死在代码里,实际业务中盖章位置往往由模板决定。建议把page/x/y/width/height抽成配置或参数,甚至支持“关键字定位”——先搜页面文本找到“盖章处”三个字的位置,再在其附近盖章。这个改造的验证方法是拿几份不同版式的文档跑一遍,看章是否都落在合理区域。
第三处是批量与并发。单文档签章和批量签章的资源管理不一样,OFDReader/OFDWriter这类对象不要跨线程共享,常见做法是每个任务独立打开文档,用线程池控制并发数。压测时重点看内存,OFD解析比较吃堆内存,批量场景下-Xmx要给够。
第四处是验签闭环。生产系统不能只签不验,建议在签章服务里加一个verify方法,签完立即自验一次,失败就回滚不输出。验签要覆盖两层:结构层(签章对象、签名描述、签名值文件是否齐全)和密码层(摘要是否匹配、签名是否有效)。下面这个表可以作为自检清单:
| 检查项 | 通过标准 | 失败时先看 |
|---|---|---|
| 签章对象 | 页面Content里有Seal节点 | 是否调了add且保存成功 |
| 印章资源 | Res.xml有对应图片条目 | 资源名是否与引用一致 |
| 签名描述 | Signatures.xml有Sign记录 | setReferences路径是否正确 |
| 签名值 | SignedValue.dat非空 | 签名是否真的执行了 |
| 密码验签 | 阅读器提示签名有效 | 证书、算法、摘要对象是否一致 |
最后说个习惯。我现在做任何签章相关的改动,都会强制走一遍“三阅读器 + 一验签 + 一解包”的流程:三个不同厂商的阅读器各开一次,点一次验签,再把文件解开看结构。这套流程救过我两次——一次是资源名大小写写错,一次是签完又动了文档导致摘要失效,都是只看单一阅读器发现不了的。签章这东西,宁可多验一遍,也别等对方打回来。希望帮到你。
本文还有配套的精品资源,点击获取