SpringBoot集成iText實(shí)現(xiàn)電子簽章功能
一 電子簽章
1.1 什么是電子簽章
基于《中華人民共和國電子簽名法》等相關(guān)法規(guī)和技術(shù)規(guī)范,具有法律效力的電子簽章一定是需要使用 CA 數(shù)字證書進(jìn)行對(duì)文件簽名,并把 CA 數(shù)字證書存放在簽名后文件中。
如果一份簽名后的電子文件中無法查看到 CA 數(shù)字證書,僅存在一個(gè)公章圖片,那么就不屬于法律意義上的電子簽名。電子簽名法規(guī)定電子文件簽署時(shí)一定要使用CA數(shù)字證書,并沒有要求一定需要含有電子印章圖片,理論上電子簽章不需要到公安局進(jìn)行備案。
實(shí)際上,電子簽章是在電子簽名技術(shù)的基礎(chǔ)上添加了印章圖像外觀,沿襲了人們所習(xí)慣的傳統(tǒng)蓋章可視效果。電子簽章使用電子簽名技術(shù)來保障電子文件內(nèi)容的防篡改性和簽署者的不可否認(rèn)性。因此,電子簽章中,印章圖片并不是唯一鑒別是否簽章的條件,還要鑒別是否使用高級(jí)電子簽名技術(shù)和 CA 數(shù)字證書。
CA 數(shù)字證書是在互聯(lián)網(wǎng)中用于識(shí)別身份的一種具有權(quán)威性的電子文檔。CA 數(shù)字證書相當(dāng)于現(xiàn)實(shí)中的身份證。
現(xiàn)實(shí)中,如同個(gè)人需要去公安局申請(qǐng)辦理身份證一樣,CA 數(shù)字證書需要在“電子認(rèn)證服務(wù)機(jī)構(gòu)”(簡(jiǎn)稱 CA 機(jī)構(gòu))進(jìn)行申請(qǐng)辦理。中國工業(yè)和信息化部、工信部授權(quán) CA 機(jī)構(gòu)來制作、簽發(fā)數(shù)字證書,用非對(duì)稱加密的方式,生成一對(duì)密碼即私鑰與公開密鑰,并綁定了數(shù)字證書持有者的真實(shí)身份,人們可以在電子合同的締約過程中用它來證明自己的身份和驗(yàn)證對(duì)方的身份。
CA 機(jī)構(gòu)頒發(fā)的數(shù)字證書為公鑰證書和私鑰證書:公鑰證書是對(duì)外公開、任何人都可以使用的,而私鑰是專屬于簽署人所有的。當(dāng)需要簽署文檔時(shí),簽署人使用私鑰證書對(duì)電子文件(文檔哈希值)進(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 原文哈希值就是電子簽名,電子簽名中有簽署人的姓名、身份證號(hào)碼、證書有效期、公鑰等信息,電子簽名放在 PDF 原文的簽名域中,就形成了帶有電子簽名的 PDF 文件。
1.2 簽名流程
文件電子簽名過程,如下圖:
其他人收到這個(gè)文件,即可使用PDF文件的簽名域中存儲(chǔ)的公鑰證書對(duì)電子簽名進(jìn)行解密,解密出來的文件哈希值如果與原文的哈希值一致,則代表這個(gè)文件沒有被篡改。
電子簽名文件驗(yàn)簽過程,如下圖:
1.3 技術(shù)選型
這塊主要有兩大技術(shù)體系:
- 開源組織 Apache 的 PDFBox。
- Adobe 的 iText,其中 iText 又分為 iText5 和 iText7。
那么這兩個(gè)該如何選擇呢?
- PDFBox 的功能相對(duì)較弱,iText5 和 iText7 的功能非常強(qiáng)悍。
- iText5 資料網(wǎng)上相對(duì)較多,如果出現(xiàn)問題容易找到解決方案。
- PDFBox 和 iText7 的網(wǎng)上資料相對(duì)較少,如果出現(xiàn)問題不易找到相關(guān)解決方案。
- PDFBox 目前提供的自定義簽章接口不完整;而 iText5 和 iText7 提供了處理自定義簽章的相關(guān)實(shí)現(xiàn)。
- PDFBox 只能實(shí)現(xiàn)把簽章圖片加簽到 PDF 文件;iText5 和 iText7 除了可以把簽章圖片加簽到 PDF 文件,還可以實(shí)現(xiàn)直接對(duì)簽章進(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 平臺(tái)提供了豐富的密碼學(xué)算法實(shí)現(xiàn),包括對(duì)稱加密、非對(duì)稱加密、哈希算法、數(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 * @微信公眾號(hào):江南一點(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); // 證書序列號(hào) 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(); // 證書序列號(hào) 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 格式存儲(chǔ)的 X.509 公鑰證書,它包含了公鑰以及證書所有者的信息,如姓名、組織、地理位置等。
keystore.p12
文件是一個(gè) PKCS#12 格式的文件,它是一個(gè)個(gè)人信息交換標(biāo)準(zhǔn),用于存儲(chǔ)一個(gè)或多個(gè)證書以及它們對(duì)應(yīng)的私鑰。.p12
文件是加密的,通常需要密碼才能打開。這種文件格式便于將證書和私鑰一起分發(fā)或存儲(chǔ),常用于需要在不同系統(tǒng)或設(shè)備間傳輸證書和私鑰的場(chǎng)景。
總結(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>
接下來對(duì) 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); // 是對(duì)應(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)申請(qǐng)的數(shù)字證書,就不會(huì)出現(xiàn)這個(gè)提示。
以上就是SpringBoot集成iText實(shí)現(xiàn)電子簽章功能的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot iText電子簽章的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring.Net在MVC中實(shí)現(xiàn)注入的原理解析
這篇文章主要介紹了Spring.Net在MVC中實(shí)現(xiàn)注入的原理解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09Struts2實(shí)現(xiàn)文件上傳功能實(shí)例解析
這篇文章主要介紹了Struts2實(shí)現(xiàn)文件上傳功能實(shí)例解析,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-01-01Java8函數(shù)式接口java.util.function速查大全
因?yàn)镴ava8引入了函數(shù)式接口,在java.util.function包含了幾大類函數(shù)式接口聲明,這篇文章主要給大家介紹了關(guān)于Java8函數(shù)式接口java.util.function速查的相關(guān)資料,需要的朋友可以參考下2021-08-08Spring深入講解實(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-05Java實(shí)現(xiàn)導(dǎo)入csv的示例代碼
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)導(dǎo)入csv的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,有需要的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03Java 并發(fā)編程學(xué)習(xí)筆記之Synchronized底層優(yōu)化
這篇文章主要介紹了Java 并發(fā)編程學(xué)習(xí)筆記之Synchronized底層優(yōu)化的相關(guān)資料,主要包含了重量級(jí)鎖,輕量級(jí)鎖,偏向鎖和其他優(yōu)化等方面,有需要的小伙伴可以參考下2016-05-05Java如何獲取對(duì)象屬性及對(duì)應(yīng)值
這篇文章主要介紹了Java如何獲取對(duì)象屬性及對(duì)應(yīng)值,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11SpringMVC整合websocket實(shí)現(xiàn)消息推送及觸發(fā)功能
這篇文章主要為大家詳細(xì)介紹了SpringMVC整合websocket實(shí)現(xiàn)消息推送及觸發(fā)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03response.setContentType()參數(shù)以及作用詳解
這篇文章主要介紹了response.setContentType()參數(shù)以及作用詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08