简介:本资源是一份基于Spring Boot实现Java电子签章功能的轻量级开发模板,面向Java后端开发者及企业级电子合同系统建设者,解决PDF合同自动化签署、数字签名嵌入与法律效力保障等核心问题。压缩包为ZIP格式,共72KB,虽未提供具体文件列表,但根据项目结构描述,应包含pom.xml依赖配置、src/main/java中的签章服务类与控制器、resources目录下的证书密钥及电子章图片等关键资产,涵盖Spring Boot启动、PDFBox/iText文档处理、JCA/JCE数字签名全流程代码。已有1249人学习下载,适合中高级Java开发者快速集成电子签章能力,掌握非对称加密应用、PDF动态盖章、证书管理及微服务化签章接口设计等实战要点,可直接用于合同SaaS模块开发或合规性改造。
1. 电子合同不是PDF盖章:Java生成电子签章的落地卡点在哪?
很多同学做毕设或课设时,一搜“电子合同 Java”,立刻跳出来一堆“用iText7给PDF加个图片章”的教程——结果答辩被导师一句“这算哪门子电子签章?法律效力在哪?时间戳怎么保证?签名验签流程走通了吗?”直接问懵。真实项目里,电子签章不是贴图,是数字签名+时间戳+CA证书链+文档哈希绑定+可验证的PKI体系。这套方案专为需要交付、能过审、真能跑通全流程的场景设计:基于Spring Boot构建后端服务,支持标准PAdES(PDF Advanced Electronic Signatures)规范,生成的PDF签名符合《电子签名法》对可靠电子签名的要求。适合高校课程设计、毕业设计、企业内部合同系统原型开发,尤其适合那些被“签名验签失败”“证书链不信任”“Adobe Reader提示签名无效”反复折磨过的同学。它不讲空泛理论,从证书申请、签名算法选型、PDF字节流注入、到Adobe兼容性校验,每一步都给出可粘贴、可调试、可截图答辩的实操路径。
2. 总体架构设计:三层结构与四个核心模块的职责划分
本方案采用清晰的分层架构,避免将签名逻辑与业务代码耦合。整体分为接入层 → 服务层 → 签名引擎层,所有与电子签章强相关的功能全部下沉至签名引擎层,确保可测试、可替换、可审计。四个核心模块并非并列关系,而是有明确调用链和数据流向:
- 证书管理模块:负责X.509证书加载、私钥解密(支持PKCS#12/PFX格式)、证书链校验、有效期检查;
- 签名计算模块:执行SHA256哈希、RSA/SM2签名运算、CRL/OCSP状态查询(可选)、时间戳请求(RFC3161);
- PDF嵌入模块:基于Apache PDFBox 2.0.27实现PAdES-LT(Long Term Validation)签名,完成签名字典构造、交叉引用表更新、增量保存;
- 验签服务模块:提供独立HTTP接口,接收已签名PDF,返回签名有效性、证书信任状态、时间戳有效性、文档完整性结论。
提示:不要在Controller里直接new SignatureService()。必须通过Spring管理Bean生命周期,否则证书私钥可能被多次加载或泄露。
2.1 为什么选PDFBox而不是iText?技术选型背后的硬约束
iText商业版虽支持PAdES,但社区版(iText 7 Core AGPL)在生成符合ETSI EN 319 142-1标准的LTV签名时存在关键缺陷:无法正确嵌入CRL/OCSP响应及时间戳令牌(TimeStampToken),导致Adobe Acrobat在“长期验证”模式下报“签名未完全验证”。而PDFBox 2.x是Apache基金会项目,完全开源,其COSDictionary底层操作能力极强,允许我们手动构造/V(签名值)、/ByteRange(字节范围数组)、/Contents(Base64编码签名)、/M(签名时间)、/Prop_Build(签名属性)等关键字段,并精确控制签名字典在PDF对象树中的位置。
实际对比测试中,同一份PDF用iText社区版签名后,在Adobe Reader DC中开启“签名面板→属性→显示签名详细信息”,会显示“签名者身份未知”且“时间戳未验证”;而PDFBox方案生成的签名,该面板明确显示“签名有效”“证书由可信CA颁发”“时间戳由权威时间戳服务机构签发”。
<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.27</version> </dependency> <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>Bouncy Castle 1.70是当前与PDFBox 2.0.27兼容性最佳的密码学库版本。低于1.69不支持SM2国密算法;高于1.70则因API变更导致CMSSignedDataGenerator构造失败。这个版本号不是随便写的,是踩过“签名后PDF打不开”“签名值解析异常”两个典型翻车点后锁定的。
2.2 证书加载与私钥解密:安全边界必须划清
电子签章的核心是私钥安全。本方案强制要求私钥以PKCS#12(.pfx/.p12)格式提供,密码由Spring配置中心(如Nacos或本地application.yml)加密存储,绝不硬编码、绝不明文写死。证书加载代码必须捕获UnrecoverableKeyException,这是私钥密码错误的唯一可靠标识。
// CertificateManager.java public class CertificateManager { private static final String KEYSTORE_TYPE = "PKCS12"; public KeyStore loadKeyStore(String pfxPath, String password) throws Exception { KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); try (InputStream is = new FileInputStream(pfxPath)) { keyStore.load(is, password.toCharArray()); } return keyStore; } public PrivateKey getPrivateKey(KeyStore keyStore, String alias, String password) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { // 必须用KeyStore.getKey(),不能用getEntry()——后者在JDK9+返回类型不同 return (PrivateKey) keyStore.getKey(alias, password.toCharArray()); } }注意:keyStore.getKey()返回的是java.security.Key,需强制转型为PrivateKey。若使用keyStore.getEntry()配合KeyStore.PrivateKeyEntry,在JDK 17环境下会因模块化限制抛出ClassCastException。这是血泪经验——某次升级JDK后,签名服务连续三天返回500,日志只显示“null”,最后发现是转型失败后getPrivateKey()返回null,后续签名计算直接NPE。
2.3 PAdES-LT签名流程:五步不可省略的操作链
PAdES-LT不是简单签名,而是构建一个可长期验证的信任链。必须严格按顺序执行以下五步,缺一不可:
- 文档哈希:对原始PDF字节流(不含签名区域)计算SHA256摘要;
- 签名生成:用私钥对摘要进行RSA-SHA256签名,得到DER编码的
SignerInfo; - 时间戳请求:向RFC3161时间戳服务器(如https://freetsa.org/tsr)发送TSA请求,获取带CA签名的时间戳令牌;
- CRL/OCSP嵌入:将证书链中每个证书对应的CRL分发点URL下载的CRL,或OCSP响应,Base64编码后存入签名字典;
- PDF增量写入:在PDF末尾追加签名对象,更新
/Root中的/AcroForm和/Perms字典,确保Adobe能识别为PAdES-LT。
其中第4步最容易被忽略。很多方案只做签名,不做CRL嵌入,导致3个月后CA吊销证书,验签即失败。本方案在PdfSignerEngine.java中封装了fetchCrlFromCert()方法,自动解析证书CRLDistributionPoints扩展项,发起HTTP GET下载CRL文件,并校验CRL签名有效性——这步耗时约300~800ms,必须异步处理或加缓存,否则接口超时。
3. 核心模块实现:签名计算与PDF嵌入的代码级细节
签名计算模块是整个方案的“心脏”,它不关心业务,只专注密码学正确性;PDF嵌入模块则是“手”,把计算结果精准安放到PDF的指定字节位置。二者必须解耦,通过DTO传递中间数据。下面展示最关键的签名计算与PDF写入两段代码,每行都有生产环境验证过的注释。
3.1 签名计算:SM2与RSA双算法支持的设计逻辑
国内政务/金融类合同常要求国密SM2算法,而国际客户多用RSA。本方案通过策略模式支持双算法切换,避免if-else污染核心逻辑。SignatureAlgorithm枚举定义了算法标识、密钥规格、签名机制:
// SignatureAlgorithm.java public enum SignatureAlgorithm { RSA_SHA256("1.2.840.113549.1.1.11", "RSA", 2048), SM2_SM3("1.2.156.10197.1.501", "SM2", 256); private final String oid; // ASN.1 Object Identifier private final String keyAlgorithm; private final int keySize; SignatureAlgorithm(String oid, String keyAlgorithm, int keySize) { this.oid = oid; this.keyAlgorithm = keyAlgorithm; this.keySize = keySize; } // getter... }签名计算主方法doSign()根据枚举自动选择Bouncy Castle的SM2Signer或RSASigner,并统一返回byte[] signatureBytes:
// SignatureCalculator.java public byte[] doSign(byte[] digestBytes, PrivateKey privateKey, SignatureAlgorithm algorithm) throws Exception { if (algorithm == SignatureAlgorithm.SM2_SM3) { // 国密SM2签名:需设置UserID(默认"1234567812345678") SM2Signer signer = new SM2Signer(); signer.init(true, new ParametersWithID( new ECPrivateKeyParameters((ECPrivateKey) privateKey, spec), Hex.decode("31323334353637383132333435363738"))); signer.update(digestBytes, 0, digestBytes.length); return signer.generateSignature(); } else { // RSA签名:标准PKCS#1 v1.5 Signature signature = Signature.getInstance("SHA256withRSA", "BC"); signature.initSign(privateKey); signature.update(digestBytes); return signature.sign(); } }关键点:SM2签名必须传入ParametersWithID,其中Hex.decode(...)是国密标准规定的默认UserID(十六进制字符串"1234567812345678")。漏掉此参数,签名值将无法被国密验签工具识别——这是某次对接某省政务平台时,对方验签失败的根本原因,查了两天才定位到。
3.2 PDF嵌入:ByteRange计算与增量保存的魔鬼细节
PDF签名最反直觉的点在于:签名值本身不能写在签名位置,而要写在PDF末尾的增量区,同时用/ByteRange数组告诉阅读器“签名覆盖哪几段字节”。这个数组必须精确到字节,差1都会导致Adobe报“签名已损坏”。
// PdfSignerEngine.java private void writeSignatureToPdf(PDDocument document, byte[] signatureBytes, SignatureAlgorithm algorithm, String reason) throws IOException { // Step 1: 计算原始PDF字节流(不含签名占位符) ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.saveIncremental(baos); // 注意!这里saveIncremental生成的是含占位符的PDF byte[] originalBytes = baos.toByteArray(); // Step 2: 定位签名占位符位置(/ByteRange中第二个数字) // 签名占位符格式:/ByteRange [0 65536 123456 65536] → 占位符长度=65536 int placeholderLength = 65536; int signatureStart = findPlaceholderStart(originalBytes); // 自定义方法,扫描"/ByteRange" // Step 3: 构造最终ByteRange数组:[0, start, start+placeholderLength, fileLength - (start+placeholderLength)] long fileLength = originalBytes.length + signatureBytes.length; long[] byteRange = new long[]{ 0L, (long) signatureStart, (long) signatureStart + placeholderLength, fileLength - ((long) signatureStart + placeholderLength) }; // Step 4: 创建签名字典 COSDictionary sigDict = new COSDictionary(); sigDict.setItem(COSName.TYPE, COSName.getPDFName("Sig")); sigDict.setItem(COSName.SUBTYPE, COSName.getPDFName("ETSI.CAdES.detached")); sigDict.setLong(COSName.M, System.currentTimeMillis()); // 签名时间 sigDict.setString(COSName.REASON, reason); sigDict.setBoolean(COSName.CONTENTS, true); sigDict.setByteArray(COSName.CONTENTS, signatureBytes); sigDict.setLongArray(COSName.BYTERANGE, byteRange); // Step 5: 增量写入签名字典和签名流 try (PDStream stream = new PDStream(document, COSName.FLATE_DECODE)) { stream.getCOSObject().setInt(COSName.LENGTH, signatureBytes.length); OutputStream os = stream.createOutputStream(COSName.FLATE_DECODE); os.write(signatureBytes); os.close(); // 将签名字典挂载到AcroForm的Fields中 PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); if (acroForm == null) { acroForm = new PDAcroForm(document); document.getDocumentCatalog().setAcroForm(acroForm); } acroForm.setNeedAppearances(true); } }这段代码的玄机在findPlaceholderStart()——它不是正则匹配,而是逐字节扫描PDF二进制流,寻找/ByteRange [后的第一个数字。因为PDF格式允许空格、换行、注释,正则极易误判。某次测试发现,当PDF含中文注释时,正则匹配偏移错位,导致ByteRange第二项比实际小3个字节,签名后Adobe直接拒绝打开。改成字节扫描后问题消失。
3.3 验签服务:不只是“验签名”,而是验信任链
验签接口/api/verify-signature返回的不是简单的true/false,而是结构化JSON,包含四层验证结论:
{ "valid": true, "signatureIntact": true, "certificateTrusted": true, "timestampValid": true, "documentModified": false, "signerInfo": { "subject": "CN=张三,OU=法务部,O=某公司,L=北京,ST=北京,C=CN", "issuer": "CN=某CA根证书,OU=Certificate Authority,O=某CA机构,C=CN", "notBefore": "2023-01-01T00:00:00Z", "notAfter": "2025-12-31T23:59:59Z" } }核心逻辑在PdfSignatureVerifier.java中,它调用Bouncy Castle的CMSSignedData解析签名,再逐层验证:
isSignatureValid():验签名值与文档哈希是否匹配;isCertificateChainTrusted():用本地信任库(truststore.jks)验证证书链;isTimestampValid():解析签名中的TimeStampToken,验证TSA证书及时间戳签名;isDocumentIntact():重新计算PDF字节流哈希,比对签名中存储的摘要。
注意:验签时必须禁用网络请求!
isCertificateChainTrusted()方法内部会关闭CRL/OCSP在线检查,仅依赖签名中已嵌入的CRL/OCSP数据。否则验签接口会因网络超时而不可用——这是线上事故高发点。
4. 常见问题排查:五个让开发者凌晨三点还在看日志的翻车点
电子签章是密码学、PDF格式、HTTP协议、证书体系的交叉领域,任何一个环节出错,现象都指向“签名无效”,但根因千差万别。以下是我在三个不同项目中亲手解决、且高频复现的五个问题,按“现象→原因→解决”结构列出,每一条都配真实日志片段。
4.1 现象:Adobe提示“签名者身份未知”,但证书链显示正常
原因:签名字典中缺失/Prop_Build(签名属性)字典,导致Adobe无法识别签名符合PAdES规范。PDFBox默认不生成此字典。
解决:在构造sigDict后,手动添加/Prop_Build:
COSDictionary propBuild = new COSDictionary(); propBuild.setItem(COSName.NAME, COSName.getPDFName("Adobe.PPKLite")); propBuild.setItem(COSName.R, new COSInteger(2)); sigDict.setItem(COSName.PROP_BUILD, propBuild);提示:
/R 2表示支持PAdES-BES(Basic Electronic Signature),这是Adobe信任的最低要求。
4.2 现象:验签返回certificateTrusted=false,但证书在浏览器中显示“受信任”
原因:验签服务使用的truststore.jks未导入CA根证书,或导入了中级CA但漏了根CA。Bouncy Castle验证链时要求完整链,从终端证书→中级CA→根CA,缺一不可。
解决:用keytool -importcert命令确认根CA已导入:
keytool -list -v -keystore truststore.jks -storepass changeit | grep "Owner:"若输出中无根CA的Owner信息,则需重新导入:
keytool -importcert -file root-ca.crt -keystore truststore.jks -alias root-ca -storepass changeit4.3 现象:签名后PDF在Foxit Reader中正常,但在Adobe Reader中显示“签名已损坏”
原因:PDFBox 2.0.27在saveIncremental()时,若原始PDF含/AcroForm但无/Fields数组,会生成非法的空/Fields,触发Adobe的严格校验。
解决:签名前强制初始化AcroForm字段:
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); if (acroForm != null && acroForm.getFields().isEmpty()) { // 创建一个空字段数组,避免PDFBox生成非法结构 acroForm.setFields(new COSArray()); }4.4 现象:SM2签名后,国密验签工具报“签名值格式错误”
原因:Bouncy Castle SM2签名返回的是DER编码的ECDSASignature(r,s),但部分国密工具要求纯字节拼接(r||s,各32字节)。
解决:对SM2签名结果做标准化处理:
if (algorithm == SignatureAlgorithm.SM2_SM3) { ASN1Sequence seq = ASN1Sequence.getInstance(signatureBytes); BigInteger r = ((ASN1Integer) seq.getObjectAt(0)).getValue(); BigInteger s = ((ASN1Integer) seq.getObjectAt(1)).getValue(); // 转为64字节:r左补零32字节 + s左补零32字节 byte[] rBytes = r.toByteArray(); byte[] sBytes = s.toByteArray(); byte[] normalized = new byte[64]; System.arraycopy(rBytes, Math.max(0, rBytes.length - 32), normalized, 32 - Math.min(32, rBytes.length), Math.min(32, rBytes.length)); System.arraycopy(sBytes, Math.max(0, sBytes.length - 32), normalized, 64 - Math.min(32, sBytes.length), Math.min(32, sBytes.length)); signatureBytes = normalized; }4.5 现象:时间戳验证失败,日志显示TSA certificate not trusted
原因:时间戳服务器(TSA)的证书未加入truststore.jks。TSA证书通常与CA根证书不同,需单独导入。
解决:先用OpenSSL获取TSA证书:
openssl s_client -connect freetsa.org:443 -servername freetsa.org 2>/dev/null | openssl x509 > tsa.crt再导入:
keytool -importcert -file tsa.crt -keystore truststore.jks -alias tsa-server -storepass changeit5. 验收与答辩:用三张截图+一份报告征服导师
答辩不是讲原理,是证明“我做的东西能跑、能验、能过审”。我带过的几个毕设项目,最终能高分通过的,都准备了这三张必拍截图和一份精炼报告。它们不是装饰,而是验收证据链。
5.1 截图一:Adobe签名面板的“绿色对勾”
打开签名后的PDF,在Adobe Reader DC中点击“签名”→“签名面板”,截取完整窗口。重点区域必须包含:
- 左上角绿色对勾图标;
- “签名者”下方显示具体姓名和单位(非“Unknown”);
- “签名时间”显示准确时间;
- 底部“签名有效性”明确写“签名有效”;
- 点击“显示签名详细信息”后,弹窗中“时间戳”栏显示“时间戳有效”且有TSA机构名称。
这张图证明:你的签名符合PAdES-LT标准,被行业事实标准Adobe认可。没有它,所有代码都是纸上谈兵。
5.2 截图二:验签接口的JSON响应
用Postman或curl调用POST /api/verify-signature,上传同一份PDF,截取返回的JSON。必须包含:
"valid": true;"certificateTrusted": true;"timestampValid": true;"signerInfo"对象中"subject"字段显示你配置的证书主题(如CN=张三)。
这张图证明:你的后端服务不仅会签,更具备完整的验签能力,且信任链验证通过。导师最怕学生只会签不会验。
5.3 截图三:控制台日志中的关键时间戳
启动Spring Boot应用,执行一次签名,截取控制台输出。必须包含:
INFO c.e.s.PdfSignerEngine - [SIGN_START] Signing document: contract_v1.pdf;INFO c.e.s.PdfSignerEngine - [TS_REQUEST] TSA request sent to https://freetsa.org/tsr;INFO c.e.s.PdfSignerEngine - [SIGN_SUCCESS] Signature completed in 1247ms。
这张图证明:你的流程是真实执行的,不是mock数据。1247ms这个数字很关键——它说明时间戳请求成功,且耗时在合理范围内(<2s)。
5.4 答辩报告:一页纸说清“为什么我的方案可靠”
不要写成论文,用表格呈现核心设计决策与依据,导师扫一眼就懂你的深度:
| 设计项 | 你的选择 | 为什么这样选?(法律/标准/工程依据) |
|---|---|---|
| 签名标准 | PAdES-LT (EN 319 142-1) | 《电子签名法》第十三条要求“签署后对数据电文内容和形式的任何改动能够被发现”,LT模式通过嵌入CRL/OCSP/TSA实现长期可验证。 |
| 密码算法 | RSA-SHA256 或 SM2-SM3 | 国密局GM/T 0003-2012、ISO/IEC 14888-1:2016均认可;SM2为国内政务强制要求。 |
| 证书格式 | PKCS#12 (.pfx) | Windows/macOS/Adobe原生支持,私钥加密存储,符合等保2.0“密钥安全存储”要求。 |
| 时间戳源 | freetsa.org (RFC3161) | 免费、稳定、符合RFC3161标准,其TSA证书由DigiCert签发,已预置于主流信任库。 |
| PDF库 | Apache PDFBox 2.0.27 | 唯一开源库能精确控制/ByteRange和增量写入,经ETSI官方测试工具验证通过。 |
从那以后我每次带学生做电子签章类毕设,都强制他们先跑通这三张截图,再写代码。因为法律效力不是写在论文里的,是印在Adobe那个绿色对勾里的。希望帮到你。
本文还有配套的精品资源,点击获取