SpringBoot集成iText實(shí)現(xiàn)電子簽章功能
一 電子簽章
1.1 什么是電子簽章
基于《中華人民共和國電子簽名法》等相關(guān)法規(guī)和技術(shù)規(guī)范,具有法律效力的電子簽章一定是需要使用 CA 數(shù)字證書進(jìn)行對文件簽名,并把 CA 數(shù)字證書存放在簽名后文件中。
如果一份簽名后的電子文件中無法查看到 CA 數(shù)字證書,僅存在一個(gè)公章圖片,那么就不屬于法律意義上的電子簽名。電子簽名法規(guī)定電子文件簽署時(shí)一定要使用CA數(shù)字證書,并沒有要求一定需要含有電子印章圖片,理論上電子簽章不需要到公安局進(jìn)行備案。
實(shí)際上,電子簽章是在電子簽名技術(shù)的基礎(chǔ)上添加了印章圖像外觀,沿襲了人們所習(xí)慣的傳統(tǒng)蓋章可視效果。電子簽章使用電子簽名技術(shù)來保障電子文件內(nèi)容的防篡改性和簽署者的不可否認(rèn)性。因此,電子簽章中,印章圖片并不是唯一鑒別是否簽章的條件,還要鑒別是否使用高級電子簽名技術(shù)和 CA 數(shù)字證書。
CA 數(shù)字證書是在互聯(lián)網(wǎng)中用于識別身份的一種具有權(quán)威性的電子文檔。CA 數(shù)字證書相當(dāng)于現(xiàn)實(shí)中的身份證。
現(xiàn)實(shí)中,如同個(gè)人需要去公安局申請辦理身份證一樣,CA 數(shù)字證書需要在“電子認(rèn)證服務(wù)機(jī)構(gòu)”(簡稱 CA 機(jī)構(gòu))進(jìn)行申請辦理。中國工業(yè)和信息化部、工信部授權(quán) CA 機(jī)構(gòu)來制作、簽發(fā)數(shù)字證書,用非對稱加密的方式,生成一對密碼即私鑰與公開密鑰,并綁定了數(shù)字證書持有者的真實(shí)身份,人們可以在電子合同的締約過程中用它來證明自己的身份和驗(yàn)證對方的身份。
CA 機(jī)構(gòu)頒發(fā)的數(shù)字證書為公鑰證書和私鑰證書:公鑰證書是對外公開、任何人都可以使用的,而私鑰是專屬于簽署人所有的。當(dāng)需要簽署文檔時(shí),簽署人使用私鑰證書對電子文件(文檔哈希值)進(jìn)行加密,形成電子簽名。 (注:文檔哈希值計(jì)算時(shí)包含待簽 PDF 文檔內(nèi)容、印章圖片和印章坐標(biāo)位置信息)
哈希值是指將 PDF 文件按照一定的算法(目前主流是 SHA256 算法),形成一個(gè)唯一的文件代碼,類似于人類的指紋,任何一個(gè) PDF 文件只有一個(gè)哈希值,且不同 PDF 文件的哈希值不可能相同,而相同哈希值的 PDF 文件的內(nèi)容肯定相同。哈希算法是不可逆的,從哈希值無法推導(dǎo)出 PDF 原文內(nèi)容。
經(jīng)簽署人的私鑰證書加密之后的 PDF 原文哈希值就是電子簽名,電子簽名中有簽署人的姓名、身份證號碼、證書有效期、公鑰等信息,電子簽名放在 PDF 原文的簽名域中,就形成了帶有電子簽名的 PDF 文件。
1.2 簽名流程
文件電子簽名過程,如下圖:

其他人收到這個(gè)文件,即可使用PDF文件的簽名域中存儲的公鑰證書對電子簽名進(jìn)行解密,解密出來的文件哈希值如果與原文的哈希值一致,則代表這個(gè)文件沒有被篡改。
電子簽名文件驗(yàn)簽過程,如下圖:

1.3 技術(shù)選型
這塊主要有兩大技術(shù)體系:
- 開源組織 Apache 的 PDFBox。
- Adobe 的 iText,其中 iText 又分為 iText5 和 iText7。
那么這兩個(gè)該如何選擇呢?
- PDFBox 的功能相對較弱,iText5 和 iText7 的功能非常強(qiáng)悍。
- iText5 資料網(wǎng)上相對較多,如果出現(xiàn)問題容易找到解決方案。
- PDFBox 和 iText7 的網(wǎng)上資料相對較少,如果出現(xiàn)問題不易找到相關(guān)解決方案。
- PDFBox 目前提供的自定義簽章接口不完整;而 iText5 和 iText7 提供了處理自定義簽章的相關(guān)實(shí)現(xiàn)。
- PDFBox 只能實(shí)現(xiàn)把簽章圖片加簽到 PDF 文件;iText5 和 iText7 除了可以把簽章圖片加簽到 PDF 文件,還可以實(shí)現(xiàn)直接對簽章進(jìn)行繪制,把文件繪制到簽章上。
- PDFBox 和 iText5/iText7 使用的協(xié)議不一樣。PDFBox 使用的是 APACHE LICENSE VERSION 2.0(Licenses);iText5/iText7 使用的是 AGPL(itextpdf.com/agpl)。PDFBox免費(fèi)使用,AGPL 商用收費(fèi)。
因此這里松哥就以 iText5 為例來和小伙伴們演示如何給一個(gè) PDF 文件簽名。
二 實(shí)戰(zhàn)
2.1 生成數(shù)字證書
首先我們需要生成一個(gè)數(shù)字證書。
這個(gè)數(shù)字證書我們可以利用 JDK 自帶的工具生成,為了貼近實(shí)戰(zhàn),松哥這里使用 Java 代碼生成,生成數(shù)字證書的方式如下。
首先引入 Bouncy Castle,Bouncy Castle 是一個(gè)廣泛使用的開源加密庫,它為 Java 平臺提供了豐富的密碼學(xué)算法實(shí)現(xiàn),包括對稱加密、非對稱加密、哈希算法、數(shù)字簽名等。這個(gè)庫由于其廣泛的算法支持和可靠性而備受信任,被許多安全應(yīng)用和加密通信協(xié)議所采用 。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15on</artifactId>
<version>1.70</version>
</dependency>
接下來我們寫一個(gè)生成數(shù)字證書的工具類,如下:
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.*;
/**
* @author:江南一點(diǎn)雨
* @site:http://www.javaboy.org
* @微信公眾號:江南一點(diǎn)雨
* @github:https://github.com/lenve
* @gitee:https://gitee.com/lenve
*/
public class PkcsUtils {
/**
* 生成證書
*
* @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ā)點(diǎn)
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());
//證書有效開始時(shí)間
result.put("notBefore", format.format(notBefore).getBytes("utf-8"));
//證書有效結(jié)束時(shí)間
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=產(chǎn)品研發(fā)部,O=江南一點(diǎn)雨,C=CN,E=javaboy@gmail.com,L=華南,ST=深圳";
String subjectStr = "CN=javaboy,OU=產(chǎn)品研發(fā)部,O=江南一點(diǎn)雨,C=CN,E=javaboy@gmail.com,L=華南,ST=深圳";
String certificateCRL = "http://www.javaboy.org";
Map<String, byte[]> result = createCert("123456", 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();
}
}
運(yùn)行這個(gè)工具代碼,會(huì)在我們當(dāng)前工程目錄下生成 keystore.p12 和 keystore.cer 兩個(gè)文件。
其中 keystore.cer 文件通常是一個(gè)以 DER 或 PEM 格式存儲的 X.509 公鑰證書,它包含了公鑰以及證書所有者的信息,如姓名、組織、地理位置等。
keystore.p12 文件是一個(gè) PKCS#12 格式的文件,它是一個(gè)個(gè)人信息交換標(biāo)準(zhǔn),用于存儲一個(gè)或多個(gè)證書以及它們對應(yīng)的私鑰。.p12 文件是加密的,通常需要密碼才能打開。這種文件格式便于將證書和私鑰一起分發(fā)或存儲,常用于需要在不同系統(tǒng)或設(shè)備間傳輸證書和私鑰的場景。
總結(jié)下就是,.cer 文件通常只包含公鑰證書,而 .p12 文件可以包含證書和私鑰。
2.2 生成印章圖片
接下來我們用 Java 代碼繪制一個(gè)簽章圖片,如下:
public class SealSample {
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("江南一點(diǎn)雨股份有限公司");
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("財(cái)務(wù)專用章");
titleFont.setSize(16);
titleFont.setSpace(8.0);
titleFont.setMargin(54);
seal.setTitleFont(titleFont);
seal.draw("公章1.png");
}
}
最終生成的簽章圖片類似下面這樣:

現(xiàn)在萬事具備,可以給 PDF 簽名了。
2.3 PDF 簽名
最后,我們可以通過如下代碼為 PDF 進(jìn)行簽名。
這里我們通過 iText 來實(shí)現(xiàn)電子簽章,因此需要先引入 iText:
<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>
接下來對 PDF 文件進(jìn)行簽名:
public class SignPdf2 {
/**
* @param password pkcs12證書密碼
* @param keyStorePath pkcs12證書路徑
* @param signPdfSrc 簽名pdf路徑
* @param signImage 簽名圖片
* @param x
* @param y
* @return
*/
public static byte[] sign(String password, String keyStorePath, String signPdfSrc, String signImage,
float x, float y) {
File signPdfSrcFile = new File(signPdfSrc);
PdfReader reader = null;
ByteArrayOutputStream signPDFData = null;
PdfStamper stp = null;
FileInputStream fos = null;
try {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
KeyStore ks = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
fos = new FileInputStream(keyStorePath);
// 私鑰密碼 為Pkcs生成證書是的私鑰密碼 123456
ks.load(fos, password.toCharArray());
String alias = (String) ks.aliases().nextElement();
PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray());
Certificate[] chain = ks.getCertificateChain(alias);
reader = new PdfReader(signPdfSrc);
signPDFData = new ByteArrayOutputStream();
// 臨時(shí)pdf文件
File temp = new File(signPdfSrcFile.getParent(), System.currentTimeMillis() + ".pdf");
stp = PdfStamper.createSignature(reader, signPDFData, '\0', temp, true);
stp.setFullCompression();
PdfSignatureAppearance sap = stp.getSignatureAppearance();
sap.setReason("數(shù)字簽名,不可改變");
// 使用png格式透明圖片
Image image = Image.getInstance(signImage);
sap.setImageScale(0);
sap.setSignatureGraphic(image);
sap.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
// 是對應(yīng)x軸和y軸坐標(biāo)
sap.setVisibleSignature(new Rectangle(x, y, x + 185, y + 68), 1,
UUID.randomUUID().toString().replaceAll("-", ""));
stp.getWriter().setCompressionLevel(5);
ExternalDigest digest = new BouncyCastleDigest();
ExternalSignature signature = new PrivateKeySignature(key, DigestAlgorithms.SHA512, provider.getName());
MakeSignature.signDetached(sap, digest, signature, chain, null, null, null, 0, MakeSignature.CryptoStandard.CADES);
stp.close();
reader.close();
return signPDFData.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (signPDFData != null) {
try {
signPDFData.close();
} catch (IOException e) {
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
}
}
}
return null;
}
public static void main(String[] args) throws Exception {
byte[] fileData = sign("123456", "keystore.p12",
"待簽名.pdf",//
"公章1.png", 100, 290);
FileOutputStream f = new FileOutputStream(new File("已簽名.pdf"));
f.write(fileData);
f.close();
}
}
這里所需要的參數(shù)基本上前文都提過了,不再多說。
從表面上看,簽名結(jié)束之后,PDF 文件上多了一個(gè)印章,如下:

本質(zhì)上,則是該 PDF 文件多了一個(gè)簽名信息,通過 Adobe 的 PDF 軟件可以查看,如下:

之所以顯示簽名有效性未知,是因?yàn)槲覀兪褂玫氖亲约荷傻臄?shù)字證書,如果從權(quán)威機(jī)構(gòu)申請的數(shù)字證書,就不會(huì)出現(xiàn)這個(gè)提示。
以上就是SpringBoot集成iText實(shí)現(xiàn)電子簽章功能的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot iText電子簽章的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring.Net在MVC中實(shí)現(xiàn)注入的原理解析
這篇文章主要介紹了Spring.Net在MVC中實(shí)現(xiàn)注入的原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
Struts2實(shí)現(xiàn)文件上傳功能實(shí)例解析
這篇文章主要介紹了Struts2實(shí)現(xiàn)文件上傳功能實(shí)例解析,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-01-01
Java8函數(shù)式接口java.util.function速查大全
因?yàn)镴ava8引入了函數(shù)式接口,在java.util.function包含了幾大類函數(shù)式接口聲明,這篇文章主要給大家介紹了關(guān)于Java8函數(shù)式接口java.util.function速查的相關(guān)資料,需要的朋友可以參考下2021-08-08
Spring深入講解實(shí)現(xiàn)AOP的三種方式
Spring的AOP就是通過動(dòng)態(tài)代理實(shí)現(xiàn)的,使用了兩個(gè)動(dòng)態(tài)代理,分別是JDK的動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理,本文重點(diǎn)給大家介紹下Spring?Aop的三種實(shí)現(xiàn),感興趣的朋友一起看看吧2022-05-05
Java實(shí)現(xiàn)導(dǎo)入csv的示例代碼
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)導(dǎo)入csv的相關(guān)知識,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,有需要的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03
Java 并發(fā)編程學(xué)習(xí)筆記之Synchronized底層優(yōu)化
這篇文章主要介紹了Java 并發(fā)編程學(xué)習(xí)筆記之Synchronized底層優(yōu)化的相關(guān)資料,主要包含了重量級鎖,輕量級鎖,偏向鎖和其他優(yōu)化等方面,有需要的小伙伴可以參考下2016-05-05
SpringMVC整合websocket實(shí)現(xiàn)消息推送及觸發(fā)功能
這篇文章主要為大家詳細(xì)介紹了SpringMVC整合websocket實(shí)現(xiàn)消息推送及觸發(fā)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03
response.setContentType()參數(shù)以及作用詳解
這篇文章主要介紹了response.setContentType()參數(shù)以及作用詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08

