👉 這是一個或許對你有用的社群
《專案實戰(影片)》:從書中學,往事上“練” 《網際網路高頻面試題》:面朝簡歷學習,春暖花開 《架構 x 系統設計》:摧枯拉朽,掌控面試高頻場景題 《精進 Java 學習指南》:系統學習,網際網路主流技術棧 《必讀 Java 原始碼專欄》:知其然,知其所以然

👉這是一個或許對你有用的開源專案國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERP、CRM、AI 大模型等等功能:
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 什麼是電子簽章
1.2 簽名流程


1.3 技術選型
-
開源組織 Apache 的 PDFBox。 -
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 商用收費。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/ruoyi-vue-pro 影片教程:https://doc.iocoder.cn/video/
二 實戰
2.1 生成數字證書
<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.p12
和 keystore.cer
兩個檔案。keystore.cer
檔案通常是一個以 DER 或 PEM 格式儲存的 X.509 公鑰證書,它包含了公鑰以及證書所有者的資訊,如姓名、組織、地理位置等。keystore.p12
檔案是一個 PKCS#12 格式的檔案,它是一個個人資訊交換標準,用於儲存一個或多個證書以及它們對應的私鑰。.p12
檔案是加密的,通常需要密碼才能開啟。這種檔案格式便於將證書和私鑰一起分發或儲存,常用於需要在不同系統或裝置間傳輸證書和私鑰的場景。.cer
檔案通常只包含公鑰證書,而 .p12
檔案可以包含證書和私鑰。2.2 生成印章圖片
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"
);
}
}
❝這裡涉及到的一些工具類文末可以下載。

2.3 PDF 簽名
<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>
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();
}
}







