在现代企业应用中,电子签章技术在文档签署、文件认证和法律效力保障中发挥着重要作用。通过 Bouncy Castle 生成数字证书来加密签章数据并验证签章合法性。本文将介绍如何在 Spring Boot 3.3 项目中集成 iText 实现电子签章功能,内容涵盖生成数字证书、绘制签章图片、项目配置和代码示例。
运行效果:
图片
图片
若想获取项目完整代码以及其他文章的项目源码,且在代码编写时遇到问题需要咨询交流,欢迎加入下方的知识星球。
使用 Bouncy Castle 生成数字证书
在生成数字证书之前,需要在 pom.xml 中添加 Bouncy Castle 的依赖:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>itext_sign_pdf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>itext_sign_pdf</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15to18</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15to18</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.4</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>5.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后,可以使用以下代码生成一个自签名的数字证书(.p12 文件),用于后续签章操作:
package com.icoderoad.util;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.*;
public class PkcsUtil {
/**
* 生成证书
*
* @return
* @throws NoSuchAlgorithmException
*/
private static KeyPair getKey() throws NoSuchAlgorithmException {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA",
new BouncyCastleProvider());
generator.initialize(1024);
// 证书中的密钥 公钥和私钥
KeyPair keyPair = generator.generateKeyPair();
return keyPair;
}
/**
* 生成证书
*
* @param password
* @param issuerStr
* @param subjectStr
* @param certificateCRL
* @return
*/
public static Map<String, byte[]> createCert(String password, String issuerStr, String subjectStr, String certificateCRL) {
Map<String, byte[]> result = new HashMap<String, byte[]>();
try(ByteArrayOutputStream out= new ByteArrayOutputStream()) {
// 标志生成PKCS12证书
KeyStore keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
keyStore.load(null, null);
KeyPair keyPair = getKey();
// issuer与 subject相同的证书就是CA证书
X509Certificate cert = generateCertificateV3(issuerStr, subjectStr,
keyPair, result, certificateCRL);
// 证书序列号
keyStore.setKeyEntry("cretkey", keyPair.getPrivate(),
password.toCharArray(), new X509Certificate[]{cert});
cert.verify(keyPair.getPublic());
keyStore.store(out, password.toCharArray());
byte[] keyStoreData = out.toByteArray();
result.put("keyStoreData", keyStoreData);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 生成证书
* @param issuerStr
* @param subjectStr
* @param keyPair
* @param result
* @param certificateCRL
* @return
*/
public static X509Certificate generateCertificateV3(String issuerStr,
String subjectStr, KeyPair keyPair, Map<String, byte[]> result,
String certificateCRL) {
ByteArrayInputStream bint = null;
X509Certificate cert = null;
try {
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
Date notBefore = new Date();
Calendar rightNow = Calendar.getInstance();
rightNow.setTime(notBefore);
// 日期加1年
rightNow.add(Calendar.YEAR, 1);
Date notAfter = rightNow.getTime();
// 证书序列号
BigInteger serial = BigInteger.probablePrime(256, new Random());
X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
new X500Name(issuerStr), serial, notBefore, notAfter,
new X500Name(subjectStr), publicKey);
JcaContentSignerBuilder jBuilder = new JcaContentSignerBuilder(
"SHA1withRSA");
SecureRandom secureRandom = new SecureRandom();
jBuilder.setSecureRandom(secureRandom);
ContentSigner singer = jBuilder.setProvider(
new BouncyCastleProvider()).build(privateKey);
// 分发点
ASN1ObjectIdentifier cRLDistributionPoints = new ASN1ObjectIdentifier(
"2.5.29.31");
GeneralName generalName = new GeneralName(
GeneralName.uniformResourceIdentifier, certificateCRL);
GeneralNames seneralNames = new GeneralNames(generalName);
DistributionPointName distributionPoint = new DistributionPointName(
seneralNames);
DistributionPoint[] points = new DistributionPoint[1];
points[0] = new DistributionPoint(distributionPoint, null, null);
CRLDistPoint cRLDistPoint = new CRLDistPoint(points);
builder.addExtension(cRLDistributionPoints, true, cRLDistPoint);
// 用途
ASN1ObjectIdentifier keyUsage = new ASN1ObjectIdentifier(
"2.5.29.15");
// | KeyUsage.nonRepudiation | KeyUsage.keyCertSign
builder.addExtension(keyUsage, true, new KeyUsage(
KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
// 基本限制 X509Extension.java
ASN1ObjectIdentifier basicConstraints = new ASN1ObjectIdentifier(
"2.5.29.19");
builder.addExtension(basicConstraints, true, new BasicConstraints(
true));
X509CertificateHolder holder = builder.build(singer);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
bint = new ByteArrayInputStream(holder.toASN1Structure()
.getEncoded());
cert = (X509Certificate) cf.generateCertificate(bint);
byte[] certBuf = holder.getEncoded();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
// 证书数据
result.put("certificateData", certBuf);
//公钥
result.put("publicKey", publicKey.getEncoded());
//私钥
result.put("privateKey", privateKey.getEncoded());
//证书有效开始时间
result.put("notBefore", format.format(notBefore).getBytes("utf-8"));
//证书有效结束时间
result.put("notAfter", format.format(notAfter).getBytes("utf-8"));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bint != null) {
try {
bint.close();
} catch (IOException e) {
}
}
}
return cert;
}
public static void main(String[] args) throws Exception {
// CN: 名字与姓氏 OU : 组织单位名称
// O :组织名称 L : 城市或区域名称 E : 电子邮件
// ST: 州或省份名称 C: 单位的两字母国家代码
String issuerStr = "CN=javaboy,OU=开发部,O=路条编程,C=CN,E=happyzjp@gmail.com,L=北京,ST=北京";
String subjectStr = "CN=javaboy,OU=开发部,O=路条编程,C=CN,E=happyzjp@gmail.com,L=北京,ST=北京";
String certificateCRL = "http://www.icoderoad.com";
Map<String, byte[]> result = createCert("89765431", issuerStr, subjectStr, certificateCRL);
FileOutputStream outPutStream = new FileOutputStream("keystore.p12");
outPutStream.write(result.get("keyStoreData"));
outPutStream.close();
FileOutputStream fos = new FileOutputStream(new File("keystore.cer"));
fos.write(result.get("certificateData"));
fos.flush();
fos.close();
}
}
运行此工具代码后,将在当前工程目录中生成两个文件:keystore.p12 和 keystore.cer。
- keystore.cer 文件通常以 DER 或 PEM 格式存储,包含 X.509 公钥证书。它不仅包含公钥,还记录了证书持有者的相关信息,如姓名、组织、地理位置等。
- keystore.p12 文件采用 PKCS#12 格式,是一种个人信息交换标准,用于存储一个或多个证书及其对应的私钥。.p12 文件是加密的,通常需要密码才能打开。这种文件格式便于在不同系统或设备之间安全地传输和存储证书和私钥。
总结来说,.cer 文件通常仅包含公钥证书,而 .p12 文件则可以包含证书及其对应的私钥。
使用 Java 代码绘制签章图片
除了数字证书,电子签章通常还需要一个可视化的签章图片。以下代码将生成一个简单的签章图片,并保存为 PNG 格式文件,供后续签章操作使用:
CreateSeal 类
package com.icoderoad.itext_sign_pdf.util;
public class CreateSeal{
public static void main(String[] args) throws Exception {
Seal seal = new Seal();
seal.setSize(200);
SealCircle sealCircle = new SealCircle();
sealCircle.setLine(4);
sealCircle.setWidth(95);
sealCircle.setHeight(95);
seal.setBorderCircle(sealCircle);
SealFont mainFont = new SealFont();
mainFont.setText("路条编程科技有限公司");
mainFont.setSize(22);
mainFont.setFamily("隶书");
mainFont.setSpace(22.0);
mainFont.setMargin(4);
seal.setMainFont(mainFont);
SealFont centerFont = new SealFont();
centerFont.setText("★");
centerFont.setSize(60);
seal.setCenterFont(centerFont);
SealFont titleFont = new SealFont();
titleFont.setText("公司专用章");
titleFont.setSize(16);
titleFont.setSpace(8.0);
titleFont.setMargin(54);
seal.setTitleFont(titleFont);
seal.draw("公司公章1.png");
}
}
以上代码会生成一个带有指定文本的签章图片,可以将图片路径配置在 application.yml 中供签章使用。此代码生成的 PNG 文件可以直接用于电子签章过程。最终生成的签章图片类似下面这样:
在这里提到的一些工具类未提供,需要通过加入星球获取。
项目依赖配置
在 Spring Boot 项目中使用 iText 实现电子签章功能,需要在 pom.xml 文件中添加相关依赖配置:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.icoderoad</groupId>
<artifactId>springboot-signature</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- iText for PDF signature -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.1.13</version>
</dependency>
<!-- Lombok for automatic getter, setter generation -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
配置文件 application.yml
为了使电子签章功能的参数更加灵活,我们在 application.yml 中设置一些配置信息,例如签章图片路径、证书路径等。通过使用 @ConfigurationProperties 读取这些配置信息,便于后续开发和维护。
signature:
image-path: "/path/to/signature.png"
certificate-path: "/path/to/certificate.p12"
certificate-password: "yourpassword"
position:
x: 400 # 默认签章X坐标
y: 50 #距离页面底部距离
配置类 SignatureProperties
@ConfigurationProperties 注解用于读取配置文件中的 signature 配置项,将其注入到配置类 SignatureProperties 中,并使用 Lombok 注解简化代码。
package com.icoderoad.itext_sign_pdf.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "signature")
public class SignatureProperties {
private String certificatePath;
private String signImage;
private String certificatePassword;
private Position position = new Position();
@Data
public static class Position {
private float x;
private float y;
}
}
配置类
package com.icoderoad.itext_sign_pdf.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 添加对/static/**路径的支持
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
}
后端代码实现:
控制器层
显示控制类
package com.icoderoad.itext_sign_pdf.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index(Model model) {
return "index";
}
}
在 SignatureController 中定义一个用于处理签章请求的接口,并通过注入 SignatureService 完成签章功能。
package com.icoderoad.itext_sign_pdf.controller;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.icoderoad.itext_sign_pdf.config.SignatureProperties;
import com.icoderoad.itext_sign_pdf.service.SignatureService;
@RestController
@RequestMapping("/api/signature")
public class SignatureController {
@Autowired
private SignatureService signatureService;
@Autowired
private SignatureProperties signatureProperties;
@PostMapping("/uploadAndSign")
public ResponseEntity<String> uploadAndSignPdf(@RequestParam("pdfFile") MultipartFile pdfFile) {
try {
// 将上传的PDF文件保存为临时文件
File tempPdfFile = convertMultiPartToFile(pdfFile);
// 使用配置文件中的参数进行签章
byte[] signedPdfData = signatureService.sign(
signatureProperties.getCertificatePassword(),
signatureProperties.getCertificatePath(),
tempPdfFile.getAbsolutePath(),
signatureProperties.getSignImage(),
signatureProperties.getPosition().getX(),
signatureProperties.getPosition().getY()
);
FileOutputStream f = new FileOutputStream(new File("已签名11.pdf"));
f.write(signedPdfData);
f.close();
// 删除临时文件
tempPdfFile.delete();
// 确定 PDF 文件的保存路径
String fileName = "签名文档.pdf";
String filePath = "src/main/resources/static/" + fileName; // 保存到 static 目录
FileOutputStream fos = new FileOutputStream(new File(filePath));
fos.write(signedPdfData);
fos.close();
// 删除临时文件
tempPdfFile.delete();
// 生成下载链接
String downloadUrl = "/static/" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString());
return ResponseEntity.ok(downloadUrl);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(500).body(null);
}
}
private File convertMultiPartToFile(MultipartFile file) throws IOException {
File convFile = new File(System.getProperty("java.io.tmpdir") + "/" + file.getOriginalFilename());
try (FileOutputStream fos = new FileOutputStream(convFile)) {
fos.write(file.getBytes());
}
return convFile;
}
}
前端页面实现
使用 Thymeleaf 和 jQuery 实现一个简单的文件上传和签章触发页面。CDN 加载 jQuery 和 Bootstrap 样式。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF 签章</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">上传 PDF 文件并添加签章</h2>
<form id="signForm">
<div class="mb-3">
<label for="pdfFile" class="form-label">选择 PDF 文件</label>
<input class="form-control" type="file" id="pdfFile" name="pdfFile" accept=".pdf" required>
</div>
<button type="submit" class="btn btn-primary">签名并获取下载链接</button>
</form>
<div id="downloadLink" class="mt-4" style="display: none;">
<h4>下载链接:</h4>
<a id="pdfDownload" href="#" target="_blank">下载签名文档</a>
</div>
</div>
<script>
$(document).ready(function () {
$('#signForm').on('submit', function (event) {
event.preventDefault();
let formData = new FormData(this);
$.ajax({
url: '/api/signature/uploadAndSign',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (response) {
// 显示下载链接
$('#downloadLink').show();
$('#pdfDownload').attr('href', response);
},
error: function (err) {
alert("签名失败,请检查输入并重试。");
}
});
});
});
</script>
</body>
</html>
结论
本文介绍了在 Spring Boot 3.3 项目中集成 iText 实现电子签章的完整流程。通过配置文件管理签章参数、使用 @ConfigurationProperties 注入配置、Lombok 简化代码,以及使用 jQuery与 Thymeleaf 搭建前端界面,我们构建了一个简单而专业的电子签章功能。