SpringBoot整合iText實現電子簽章

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本 

一 電子簽章

1.1 什麼是電子簽章

基於《中華人民共和國電子簽名法》等相關法規和技術規範,具有法律效力的電子簽章一定是需要使用 CA 數字證書進行對檔案簽名,並把 CA 數字證書存放在簽名後文件中。
如果一份簽名後的電子檔案中無法檢視到 CA 數字證書,僅存在一個公章圖片,那麼就不屬於法律意義上的電子簽名。電子簽名法規定電子檔案簽署時一定要使用CA數字證書,並沒有要求一定需要含有電子印章圖片,理論上電子簽章不需要到公安局進行備案。
實際上,電子簽章是在電子簽名技術的基礎上添加了印章影像外觀,沿襲了人們所習慣的傳統蓋章可視效果。電子簽章使用電子簽名技術來保障電子檔案內容的防篡改性和簽署者的不可否認性。因此,電子簽章中,印章圖片並不是唯一鑑別是否簽章的條件,還要鑑別是否使用高階電子簽名技術和 CA 數字證書。
CA 數字證書是在網際網路中用於識別身份的一種具有權威性的電子文件。CA 數字證書相當於現實中的身份證。
現實中,如同個人需要去公安局申請辦理身份證一樣,CA 數字證書需要在“電子認證服務機構”(簡稱 CA 機構)進行申請辦理。中國工業和資訊化部、工信部授權 CA 機構來製作、簽發數字證書,用非對稱加密的方式,生成一對密碼即私鑰與公開金鑰,並綁定了數字證書持有者的真實身份,人們可以在電子合同的締約過程中用它來證明自己的身份和驗證對方的身份。
CA 機構頒發的數字證書為公鑰證書和私鑰證書:公鑰證書是對外公開、任何人都可以使用的,而私鑰是專屬於簽署人所有的。當需要簽署文件時,簽署人使用私鑰證書對電子檔案(文件雜湊值)進行加密,形成電子簽名。 (注:文件雜湊值計算時包含待籤 PDF 文件內容、印章圖片和印章座標位置資訊)
雜湊值是指將 PDF 檔案按照一定的演算法(目前主流是 SHA256 演算法),形成一個唯一的檔案程式碼,類似於人類的指紋,任何一個 PDF 檔案只有一個雜湊值,且不同 PDF 檔案的雜湊值不可能相同,而相同雜湊值的 PDF 檔案的內容肯定相同。雜湊演算法是不可逆的,從雜湊值無法推匯出 PDF 原文內容。
經簽署人的私鑰證書加密之後的 PDF 原文雜湊值就是電子簽名,電子簽名中有簽署人的姓名、身份證號碼、證書有效期、公鑰等資訊,電子簽名放在 PDF 原文的簽名域中,就形成了帶有電子簽名的 PDF 檔案。

1.2 簽名流程

檔案電子簽名過程,如下圖:
其他人收到這個檔案,即可使用PDF檔案的簽名域中儲存的公鑰證書對電子簽名進行解密,解密出來的檔案雜湊值如果與原文的雜湊值一致,則代表這個檔案沒有被篡改。
電子簽名檔案驗簽過程,如下圖:

1.3 技術選型

這塊主要有兩大技術體系:
  1. 開源組織 Apache 的 PDFBox。
  2. Adobe 的 iText,其中 iText 又分為 iText5 和 iText7。
那麼這兩個該如何選擇呢?
  • PDFBox 的功能相對較弱,iText5 和 iText7 的功能非常強悍。
  • iText5 資料網上相對較多,如果出現問題容易找到解決方案。
  • PDFBox 和 iText7 的網上資料相對較少,如果出現問題不易找到相關解決方案。
  • PDFBox 目前提供的自定義簽章介面不完整;而 iText5 和 iText7 提供了處理自定義簽章的相關實現。
  • PDFBox 只能實現把簽章圖片加簽到 PDF 檔案;iText5 和 iText7 除了可以把簽章圖片加簽到 PDF 檔案,還可以實現直接對簽章進行繪製,把檔案繪製到簽章上。
  • PDFBox 和 iText5/iText7 使用的協議不一樣。PDFBox 使用的是 APACHE LICENSE VERSION 2.0(Licenses);iText5/iText7 使用的是 AGPL(https://itextpdf.com/agpl)。PDFBox 免費使用,AGPL 商用收費。
因此這裡松哥就以 iText5 為例來和小夥伴們演示如何給一個 PDF 檔案簽名。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

二 實戰

2.1 生成數字證書

首先我們需要生成一個數字證書。
這個數字證書我們可以利用 JDK 自帶的工具生成,為了貼近實戰,松哥這裡使用 Java 程式碼生成,生成數字證書的方式如下。
首先引入 Bouncy Castle,Bouncy Castle 是一個廣泛使用的開源加密庫,它為 Java 平臺提供了豐富的密碼學演算法實現,包括對稱加密、非對稱加密、雜湊演算法、數字簽名等。這個庫由於其廣泛的演算法支援和可靠性而備受信任,被許多安全應用和加密通訊協議所採用 。
<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>

接下來我們寫一個生成數字證書的工具類,如下:
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.*;

publicclassPkcsUtils

{

/**

   * 生成證書

   *

   * 

@return

   * 

@throws

 NoSuchAlgorithmException

   */


privatestatic 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

   */


publicstatic

 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

   */


publicstatic 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;

  }

publicstaticvoidmain(String[] args)throws Exception 

{

// CN: 名字與姓氏    OU : 組織單位名稱
// O :組織名稱  L : 城市或區域名稱  E : 電子郵件
// ST: 州或省份名稱  C: 單位的兩字母國家程式碼

    String issuerStr = 

"CN=javaboy,OU=產品研發部,O=江南一點雨,C=CN,[email protected],L=華南,ST=深圳"

;

    String subjectStr = 

"CN=javaboy,OU=產品研發部,O=江南一點雨,C=CN,[email protected],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();

  }
}

執行這個工具程式碼,會在我們當前工程目錄下生成 keystore.p12keystore.cer 兩個檔案。
其中 keystore.cer 檔案通常是一個以 DER 或 PEM 格式儲存的 X.509 公鑰證書,它包含了公鑰以及證書所有者的資訊,如姓名、組織、地理位置等。
keystore.p12 檔案是一個 PKCS#12 格式的檔案,它是一個個人資訊交換標準,用於儲存一個或多個證書以及它們對應的私鑰。.p12 檔案是加密的,通常需要密碼才能開啟。這種檔案格式便於將證書和私鑰一起分發或儲存,常用於需要在不同系統或裝置間傳輸證書和私鑰的場景。
總結下就是,.cer 檔案通常只包含公鑰證書,而 .p12 檔案可以包含證書和私鑰。

2.2 生成印章圖片

接下來我們用 Java 程式碼繪製一個簽章圖片,如下:
publicclassSealSample

{

publicstaticvoidmain(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"

);

  }

}

這裡涉及到的一些工具類文末可以下載。
最終生成的簽章圖片類似下面這樣:
現在萬事具備,可以給 PDF 簽名了。

2.3 PDF 簽名

最後,我們可以透過如下程式碼為 PDF 進行簽名。
這裡我們透過 iText 來實現電子簽章,因此需要先引入 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 檔案進行簽名:
publicclassSignPdf2

{

/**

   * 

@param

 password pkcs12證書密碼

   * 

@param

 keyStorePath pkcs12證書路徑

   * 

@param

 signPdfSrc 簽名pdf路徑

   * 

@param

 signImage 簽名圖片

   * 

@param

 x

   * 

@param

 y

   * 

@return

   */


publicstaticbyte

[] 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();

// 臨時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(

"數字簽名,不可改變"

);

// 使用png格式透明圖片

      Image image = Image.getInstance(signImage);

      sap.setImageScale(

0

);

      sap.setSignatureGraphic(image);

      sap.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);

// 是對應x軸和y軸座標

      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) {

        }

      }

    }

returnnull

;

  }

publicstaticvoidmain(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();

  }

}

這裡所需要的引數基本上前文都提過了,不再多說。
從表面上看,簽名結束之後,PDF 檔案上多了一個印章,如下:
本質上,則是該 PDF 檔案多了一個簽名信息,透過 Adobe 的 PDF 軟體可以檢視,如下:
之所以顯示簽名有效性未知,是因為我們使用的是自己生成的數字證書,如果從權威機構申請的數字證書,就不會出現這個提示。
好啦,是不是很 easy?

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章