專業(yè)!Spring Boot 3.3 集成 iText 實現(xiàn)高效電子簽章
在現(xiàn)代企業(yè)應(yīng)用中,電子簽章技術(shù)在文檔簽署、文件認(rèn)證和法律效力保障中發(fā)揮著重要作用。通過 Bouncy Castle 生成數(shù)字證書來加密簽章數(shù)據(jù)并驗證簽章合法性。本文將介紹如何在 Spring Boot 3.3 項目中集成 iText 實現(xiàn)電子簽章功能,內(nèi)容涵蓋生成數(shù)字證書、繪制簽章圖片、項目配置和代碼示例。
運行效果:
圖片
圖片
若想獲取項目完整代碼以及其他文章的項目源碼,且在代碼編寫時遇到問題需要咨詢交流,歡迎加入下方的知識星球。
使用 Bouncy Castle 生成數(shù)字證書
在生成數(shù)字證書之前,需要在 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>
然后,可以使用以下代碼生成一個自簽名的數(shù)字證書(.p12 文件),用于后續(xù)簽章操作:
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()) {
// 標(biāo)志生成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);
// 分發(fā)點
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");
// 證書數(shù)據(jù)
result.put("certificateData", certBuf);
//公鑰
result.put("publicKey", publicKey.getEncoded());
//私鑰
result.put("privateKey", privateKey.getEncoded());
//證書有效開始時間
result.put("notBefore", format.format(notBefore).getBytes("utf-8"));
//證書有效結(jié)束時間
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 : 城市或區(qū)域名稱 E : 電子郵件
// ST: 州或省份名稱 C: 單位的兩字母國家代碼
String issuerStr = "CN=javaboy,OU=開發(fā)部,O=路條編程,C=CN,E=happyzjp@gmail.com,L=北京,ST=北京";
String subjectStr = "CN=javaboy,OU=開發(fā)部,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();
}
}
運行此工具代碼后,將在當(dāng)前工程目錄中生成兩個文件:keystore.p12 和 keystore.cer。
- keystore.cer 文件通常以 DER 或 PEM 格式存儲,包含 X.509 公鑰證書。它不僅包含公鑰,還記錄了證書持有者的相關(guān)信息,如姓名、組織、地理位置等。
- keystore.p12 文件采用 PKCS#12 格式,是一種個人信息交換標(biāo)準(zhǔn),用于存儲一個或多個證書及其對應(yīng)的私鑰。.p12 文件是加密的,通常需要密碼才能打開。這種文件格式便于在不同系統(tǒng)或設(shè)備之間安全地傳輸和存儲證書和私鑰。
總結(jié)來說,.cer 文件通常僅包含公鑰證書,而 .p12 文件則可以包含證書及其對應(yīng)的私鑰。
使用 Java 代碼繪制簽章圖片
除了數(shù)字證書,電子簽章通常還需要一個可視化的簽章圖片。以下代碼將生成一個簡單的簽章圖片,并保存為 PNG 格式文件,供后續(xù)簽章操作使用:
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 實現(xiàn)電子簽章功能,需要在 pom.xml 文件中添加相關(guān)依賴配置:
<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
為了使電子簽章功能的參數(shù)更加靈活,我們在 application.yml 中設(shè)置一些配置信息,例如簽章圖片路徑、證書路徑等。通過使用 @ConfigurationProperties 讀取這些配置信息,便于后續(xù)開發(fā)和維護(hù)。
signature:
image-path: "/path/to/signature.png"
certificate-path: "/path/to/certificate.p12"
certificate-password: "yourpassword"
position:
x: 400 # 默認(rèn)簽章X坐標(biāo)
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/");
}
}
后端代碼實現(xiàn):
控制器層
顯示控制類
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);
// 使用配置文件中的參數(shù)進(jìn)行簽章
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;
}
}
前端頁面實現(xiàn)
使用 Thymeleaf 和 jQuery 實現(xiàn)一個簡單的文件上傳和簽章觸發(fā)頁面。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" >
<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>
結(jié)論
本文介紹了在 Spring Boot 3.3 項目中集成 iText 實現(xiàn)電子簽章的完整流程。通過配置文件管理簽章參數(shù)、使用 @ConfigurationProperties 注入配置、Lombok 簡化代碼,以及使用 jQuery與 Thymeleaf 搭建前端界面,我們構(gòu)建了一個簡單而專業(yè)的電子簽章功能。