# EasySign **Repository Path**: duxvfeng/EasySign ## Basic Information - **Project Name**: EasySign - **Description**: EasySignEasySignEasySignEasySignEasySignEasySign - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-03 - **Last Updated**: 2026-05-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # EasySign - 电子签章Spring Boot Starter ## 项目简介 EasySign是一个基于Apache PDFBox的电子签章Spring Boot Starter,提供了简单易用的API来对PDF文档进行数字签名和验证。 ### 主要特性 - 支持多种签章类型:文本签名、图片签名、图文混合签名、不可见签名 - 基于标准的PKCS12证书格式 - 完整的签名验证功能 - 与Spring Boot无缝集成 - 适配JDK 17+ - 支持自定义签章位置、大小、颜色等 --- ## 项目结构 ``` EasySign/ ├── pom.xml # 父POM ├── easy-sign-core/ # 核心功能模块 │ ├── pom.xml │ └── src/main/java/com/easysign/core/ │ ├── exception/ # 异常类 │ │ └── SignException.java │ ├── enums/ # 枚举类 │ │ └── SignatureType.java │ ├── model/ # 数据模型 │ │ ├── SignaturePosition.java │ │ ├── SignatureRequest.java │ │ └── SignatureResult.java │ ├── service/ # 核心服务 │ │ ├── PdfSigner.java │ │ └── SignatureVerifier.java │ └── util/ # 工具类 │ ├── CertificateManager.java │ └── CertificateGenerator.java ├── easy-sign-autoconfigure/ # 自动配置模块 │ ├── pom.xml │ └── src/main/ │ ├── java/com/easysign/autoconfigure/ │ │ ├── EasySignAutoConfiguration.java │ │ ├── properties/ │ │ │ └── EasySignProperties.java │ │ └── template/ │ │ └── EasySignTemplate.java │ └── resources/META-INF/spring/ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ├── easy-sign-starter/ # Spring Boot Starter │ └── pom.xml └── easy-sign-demo/ # 使用示例 ├── pom.xml └── src/main/ ├── java/com/easysign/demo/ │ ├── EasySignDemoApplication.java │ ├── controller/ │ │ └── SignController.java │ ├── service/ │ │ └── SignDemoService.java │ └── runner/ │ └── DemoRunner.java └── resources/ └── application.yml ``` --- ## 快速开始 ### 环境要求 - JDK 17+ - Maven 3.6+ - Spring Boot 3.2.x ### 步骤1: 编译项目 ```bash cd e:\code\EasySign mvn clean install ``` ### 步骤2: 生成测试证书 首次使用需要生成测试证书。项目提供了证书生成工具: ```java // 运行CertificateGenerator的main方法 // 或者在启动demo项目时自动生成 ``` 或者使用keytool命令: ```bash keytool -genkeypair -alias test-alias -keyalg RSA -keysize 2048 \ -storetype PKCS12 -keystore certs/test.p12 -storepass 123456 \ -dname "CN=Test User, OU=Test, O=Test Org, L=Beijing, ST=Beijing, C=CN" ``` ### 步骤3: 添加依赖 在你的Spring Boot项目中添加依赖: ```xml com.easysign easy-sign-starter 1.0.0 ``` ### 步骤4: 配置参数 在`application.yml`中配置: ```yaml easysign: enabled: true certificate: key-store-path: certs/test.p12 # PKCS12证书路径 key-store-password: 123456 # 密钥库密码 key-alias: test-alias # 证书别名 key-password: 123456 # 密钥密码(可选) default-signature: signer-name: Test User # 默认签名者名称 reason: Document Approval # 默认签名原因 location: Beijing # 默认签名位置 visible: true # 默认是否可见 font-size: 14 # 默认字体大小 font-color: "#000000" # 默认字体颜色 ``` ### 步骤5: 使用示例 #### 5.1 基本文本签名 ```java @Autowired private EasySignTemplate easySignTemplate; public void signPdf() { // 方式1: 使用便捷方法 SignatureResult result = easySignTemplate.signText( "input.pdf", // 源PDF路径 "output_signed.pdf", // 输出PDF路径 "John Doe - CEO", // 签名文本 50, 50, 200, 60 // x, y, width, height ); if (result.isSuccess()) { System.out.println("签名成功: " + result.getOutputPath()); } } ``` #### 5.2 指定页码的签名 ```java SignatureResult result = easySignTemplate.signText( "input.pdf", "output_signed.pdf", "Approved by Legal", 2, // 页码(从1开始) 400, 50, 180, 50 // x, y, width, height ); ``` #### 5.3 图片签名 ```java SignatureResult result = easySignTemplate.signImage( "input.pdf", "output_signed.pdf", "signature.png", // 签章图片路径 100, 100, 150, 80 // 位置和大小 ); ``` #### 5.4 不可见签名(仅证书) ```java SignatureResult result = easySignTemplate.signInvisible( "input.pdf", "output_signed.pdf" ); ``` #### 5.5 使用完整的请求对象 ```java SignatureRequest request = new SignatureRequest(); request.setSourcePdfPath("input.pdf"); request.setOutputPdfPath("output.pdf"); request.setType(SignatureType.TEXT_AND_IMAGE); request.setTextContent("Approved"); request.setImagePath("stamp.png"); request.setPosition(new SignaturePosition(1, 50, 50, 200, 80)); request.setSignerName("John Doe"); request.setReason("Document Approval"); request.setLocation("Beijing"); request.setFontSize(16); request.setFontColor("#FF0000"); SignatureResult result = easySignTemplate.sign(request); ``` #### 5.6 验证签名 ```java // 验证PDF中的所有签名 List results = easySignTemplate.verify("signed.pdf"); for (SignatureVerifier.VerificationResult result : results) { System.out.println("签名有效性: " + result.isValid()); System.out.println("签名者: " + result.getSignatureName()); System.out.println("签名时间: " + result.getSignTime()); System.out.println("证书主题: " + result.getSubjectDN()); } // 快速检查是否有有效签名 boolean hasValid = easySignTemplate.hasValidSignature("signed.pdf"); ``` #### 5.7 使用字节数组(适合Web上传场景) ```java @PostMapping("/sign") public byte[] signPdf(@RequestParam("file") MultipartFile file) throws IOException { SignatureRequest request = new SignatureRequest(); request.setSourcePdfBytes(file.getBytes()); request.setType(SignatureType.TEXT); request.setTextContent("Signed"); request.setPosition(new SignaturePosition(50, 50, 150, 60)); return easySignTemplate.signToBytes(request); } ``` --- ## API参考 ### EasySignTemplate 主要方法 | 方法 | 描述 | |------|------| | `signText(source, output, text, x, y, w, h)` | 基本文本签名 | | `signText(source, output, text, page, x, y, w, h)` | 指定页码的文本签名 | | `signImage(source, output, imagePath, x, y, w, h)` | 图片签名 | | `signInvisible(source, output)` | 不可见签名 | | `sign(request)` | 使用完整请求对象签名 | | `signToBytes(request)` | 签名并返回字节数组 | | `verify(pdfPath)` | 验证PDF中的签名 | | `verify(bytes)` | 从字节数组验证签名 | | `hasValidSignature(pdfPath)` | 检查是否有有效签名 | ### SignatureType 签名类型 | 类型 | 描述 | |------|------| | `TEXT` | 纯文本签名 | | `IMAGE` | 纯图片签名 | | `TEXT_AND_IMAGE` | 图文混合签名 | | `CERT_ONLY` | 仅证书(不可见)签名 | ### SignatureResult 签名结果 | 字段 | 描述 | |------|------| | `success` | 是否成功 | | `message` | 结果消息 | | `outputPath` | 输出文件路径 | | `outputBytes` | 输出字节数组 | | `signTime` | 签名时间 | | `signerName` | 签名者名称 | | `reason` | 签名原因 | | `location` | 签名位置 | ### VerificationResult 验证结果 | 字段 | 描述 | |------|------| | `valid` | 签名是否有效 | | `message` | 验证消息 | | `signatureName` | 签名名称 | | `reason` | 签名原因 | | `location` | 签名位置 | | `signTime` | 签名时间 | | `subjectDN` | 证书主题 | | `issuerDN` | 证书颁发者 | | `certificate` | X509证书对象 | --- ## 验证案例 ### 案例1: 基本文本签名 **场景**: 对合同PDF进行文本签名 ```java @Test void testTextSignature() { // 1. 准备测试PDF String inputPdf = "contract.pdf"; String outputPdf = "contract_signed.pdf"; // 2. 执行签名 SignatureResult result = easySignTemplate.signText( inputPdf, outputPdf, "张三 - 总经理", 400, 100, 180, 60 ); // 3. 验证结果 assertTrue(result.isSuccess()); assertNotNull(result.getOutputPath()); // 4. 验证签名有效性 List verifyResults = easySignTemplate.verify(outputPdf); assertEquals(1, verifyResults.size()); assertTrue(verifyResults.get(0).isValid()); } ``` ### 案例2: 多页文档签名 **场景**: 在多页文档的不同位置签名 ```java @Test void testMultiPageSignature() { String inputPdf = "multi_page.pdf"; String outputPdf = "multi_page_signed.pdf"; // 第1页底部签名 SignatureRequest request1 = new SignatureRequest(); request1.setSourcePdfPath(inputPdf); request1.setType(SignatureType.TEXT); request1.setTextContent("Page 1 Signature"); request1.setPosition(new SignaturePosition(1, 50, 50, 150, 50)); // 先签第1页... // 第3页右下角签名 SignatureRequest request2 = new SignatureRequest(); request2.setSourcePdfPath(outputPdf); request2.setType(SignatureType.TEXT); request2.setTextContent("Approved"); request2.setPosition(new SignaturePosition(3, 450, 50, 100, 40)); // 再签第3页... // 验证多个签名 List results = easySignTemplate.verify(outputPdf); assertEquals(2, results.size()); } ``` ### 案例3: 图片签章(公章) **场景**: 使用公章图片进行签名 ```java @Test void testImageSignature() { String inputPdf = "document.pdf"; String outputPdf = "document_stamped.pdf"; String stampImage = "company_stamp.png"; SignatureResult result = easySignTemplate.signImage( inputPdf, outputPdf, stampImage, 300, 200, 120, 120 // 圆形公章通常120x120 ); assertTrue(result.isSuccess()); // 验证 List verifyResults = easySignTemplate.verify(outputPdf); assertTrue(verifyResults.get(0).isValid()); } ``` ### 案例4: 不可见签名 **场景**: 只需要数字证书保护,不需要视觉标识 ```java @Test void testInvisibleSignature() { String inputPdf = "sensitive.pdf"; String outputPdf = "sensitive_signed.pdf"; // 不可见签名 - 只附加数字证书 SignatureResult result = easySignTemplate.signInvisible( inputPdf, outputPdf ); assertTrue(result.isSuccess()); // 验证 - 虽然视觉不可见,但数字签名有效 List verifyResults = easySignTemplate.verify(outputPdf); assertTrue(verifyResults.get(0).isValid()); assertEquals("Document Authentication", verifyResults.get(0).getReason()); } ``` ### 案例5: 批量签名 **场景**: 对多个PDF文件进行批量签名 ```java @Test void testBatchSigning() { List pdfFiles = Arrays.asList( "contract1.pdf", "contract2.pdf", "contract3.pdf" ); List signedFiles = new ArrayList<>(); for (String pdf : pdfFiles) { String output = pdf.replace(".pdf", "_signed.pdf"); SignatureResult result = easySignTemplate.signText( pdf, output, "批量签名", 50, 50, 150, 50 ); if (result.isSuccess()) { signedFiles.add(output); } } // 验证所有签名 for (String signed : signedFiles) { assertTrue(easySignTemplate.hasValidSignature(signed)); } } ``` --- ## REST API示例 ### 测试服务状态 ```bash GET http://localhost:8080/api/sign/test 响应: { "status": "ok", "message": "EasySign服务运行正常" } ``` ### 文本签名 ```bash POST http://localhost:8080/api/sign/text Content-Type: multipart/form-data 参数: - file: PDF文件 - text: 签名文本(可选, 默认"Digital Signature") - x: X坐标(可选, 默认50) - y: Y坐标(可选, 默认50) - width: 宽度(可选, 默认200) - height: 高度(可选, 默认80) 示例: curl -X POST \ -F "file=@contract.pdf" \ -F "text=Approved by CEO" \ -F "x=400" \ -F "y=50" \ http://localhost:8080/api/sign/text \ -o contract_signed.pdf ``` ### 图片签名 ```bash POST http://localhost:8080/api/sign/image Content-Type: multipart/form-data 参数: - file: PDF文件 - imageFile: 签章图片 - x, y, width, height: 位置和大小 示例: curl -X POST \ -F "file=@document.pdf" \ -F "imageFile=@stamp.png" \ http://localhost:8080/api/sign/image \ -o document_signed.pdf ``` ### 不可见签名 ```bash POST http://localhost:8080/api/sign/invisible Content-Type: multipart/form-data 参数: - file: PDF文件 示例: curl -X POST \ -F "file=@sensitive.pdf" \ http://localhost:8080/api/sign/invisible \ -o sensitive_signed.pdf ``` ### 验证签名 ```bash POST http://localhost:8080/api/sign/verify Content-Type: multipart/form-data 参数: - file: 已签名的PDF文件 示例: curl -X POST \ -F "file=@signed.pdf" \ http://localhost:8080/api/sign/verify 响应示例: { "totalSignatures": 2, "hasValidSignature": true, "signatures": [ { "valid": true, "message": "签名验证通过", "signatureName": "Test User", "reason": "Document Approval", "location": "Beijing", "filter": "Adobe.PPKLite", "subFilter": "adbe.pkcs7.detached", "signTime": "2026-05-03T10:30:00", "subjectDN": "CN=Test User, O=Test Org", "issuerDN": "CN=Test User, O=Test Org" } ] } ``` --- ## 配置详解 ### 证书配置 | 配置项 | 类型 | 必填 | 说明 | |--------|------|------|------| | `easysign.certificate.key-store-path` | String | 是 | PKCS12证书文件路径 | | `easysign.certificate.key-store-password` | String | 是 | 密钥库访问密码 | | `easysign.certificate.key-alias` | String | 否 | 证书别名,不填则使用第一个别名 | | `easysign.certificate.key-password` | String | 否 | 密钥密码,不填则使用密钥库密码 | ### 默认签名配置 | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | `easysign.default-signature.signer-name` | String | null | 默认签名者名称 | | `easysign.default-signature.reason` | String | null | 默认签名原因 | | `easysign.default-signature.location` | String | null | 默认签名位置 | | `easysign.default-signature.visible` | boolean | true | 是否可见 | | `easysign.default-signature.font-size` | float | 12 | 字体大小 | | `easysign.default-signature.font-color` | String | "#000000" | 字体颜色(十六进制) | | `easysign.default-signature.image-path` | String | null | 默认签章图片路径 | | `easysign.default-signature.timestamp-url` | String | null | 时间戳服务URL | --- ## 技术说明 ### 数字签名原理 EasySign使用标准的PDF数字签名机制: 1. **证书管理**: 使用PKCS12格式的证书库,包含私钥和证书链 2. **签名算法**: SHA-256 with RSA 3. **PDF签名格式**: Adobe.PPKLite / adbe.pkcs7.detached 4. **验证机制**: 使用Bouncy Castle库验证CMS签名数据 ### 坐标系统 PDF使用左下角为原点的坐标系统: ``` Y轴 ^ | | (x + width, y + height) | +------------------+ | | | | | 签名区域 | | | | | +------------------+ | (x, y) | +----------------------------------> X轴 (0, 0) ``` ### 依赖库 | 库 | 版本 | 用途 | |---|------|------| | Apache PDFBox | 2.0.29 | PDF文档处理 | | Bouncy Castle | 1.77 | 密码学操作 | | Spring Boot | 3.2.5 | 自动配置和依赖注入 | --- ## 常见问题 ### Q1: 证书生成后无法使用 **解决方案**: - 确保证书格式为PKCS12(.p12或.pfx) - 检查密码是否正确 - 使用keytool查看证书信息: ```bash keytool -list -v -storetype PKCS12 -keystore test.p12 ``` ### Q2: 中文显示乱码 **原因**: PDFBox内置字体不支持中文 **解决方案**: - 使用图片签章替代文本签章 - 或扩展PdfSigner类,嵌入中文字体 ### Q3: 签名后PDF无法打开 **可能原因**: - 原始PDF已加密 - PDF格式有问题 **解决方案**: - 先解密PDF再签名 - 使用标准PDF工具检查PDF有效性 ### Q4: 签名验证失败 **可能原因**: - PDF在签名后被修改 - 证书已过期或被撤销 - 时间戳服务不可用 **排查步骤**: 1. 检查PDF是否被篡改 2. 验证证书有效性 3. 检查签名时间 ### Q5: 如何在生产环境使用正式证书 **步骤**: 1. 向CA机构申请正式的数字证书 2. 将证书导出为PKCS12格式 3. 配置证书路径和密码 4. 考虑配置时间戳服务(TSA) --- ## 扩展开发 ### 自定义签名外观 继承或修改`PdfSigner`类的`createVisualSignature`方法来自定义签名外观。 ### 添加时间戳支持 配置`timestamp-url`参数,使用RFC 3161时间戳服务: ```yaml easysign: default-signature: timestamp-url: http://timestamp.digicert.com ``` ### 多证书支持 创建多个`CertificateManager`实例,使用不同的证书进行签名: ```java CertificateManager cert1 = new CertificateManager("cert1.p12", "pass1", "alias1", "pass1"); CertificateManager cert2 = new CertificateManager("cert2.p12", "pass2", "alias2", "pass2"); PdfSigner signer1 = new PdfSigner(cert1); PdfSigner signer2 = new PdfSigner(cert2); ``` --- ## 版本历史 ### v1.0.0 (2026-05-03) - 初始版本发布 - 支持文本签名、图片签名、不可见签名 - 完整的签名验证功能 - Spring Boot Starter自动配置 - JDK 17+支持 --- ## 许可证 本项目遵循MIT许可证。 --- ## 联系方式 如有问题或建议,请提交Issue。