項目源碼路徑:https://github.com/Syske/learning-dome-code.git
前言
最近應客戶需求,需要實現電子簽章功能,公章部分用的時金格科技的接口,個人人簽字需要自己實現,公章部分我們就不說了,商業接口做的都比較成熟,也有示例代碼,所以今天着重說的就是個人簽字部分。
參照公章部分的實現方式,同時也參考了很多博客1,查了很多資料,也搞清楚了電子簽章的基本流程:
因為我要實現的功能很簡單就是個人簽章,而且我的簽名是手寫的,所以創建簽名部分就省略了,核心部分就是確定簽名坐標和簽名,確定坐標部分我根據自己查找的資料,實現了根據關鍵字確定坐標,因為確定坐標很麻煩,也不夠靈活。
對於創建簽名我有一個思路,可以將創建簽名作為一個遠程服務部署,然后遠程調用,然后檢驗,返回簽名;當然你也可以通過這種方式生成公章,但是因為沒有經過公正機構認證,這種方式生成的公章其實是不具法律效應的,好了下面直接上代碼吧。
創建簽名密鑰
這個密鑰在pdf簽名的時候會校驗
package io.github.syske.common.util;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
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.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.*;
import java.security.cert.Certificate;
/**
* @program: dpf-sign-test
* @description: 簽名工具類
* @author: syske
* @create: 2019-12-04 09:02
*/
public class Pkcs {
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[]>();
ByteArrayOutputStream out = null;
try {
// 生成JKS證書
// KeyStore keyStore = KeyStore.getInstance("JKS");
// 標志生成PKCS12證書
KeyStore keyStore = KeyStore.getInstance("PKCS12",
new BouncyCastleProvider());
keyStore.load(null, null);
KeyPair keyPair = getKey();
// issuer與 subject相同的證書就是CA證書
Certificate cert = generateCertificateV3(issuerStr, subjectStr,
keyPair, result, certificateCRL, null);
// cretkey隨便寫,標識別名
keyStore.setKeyEntry("cretkey", keyPair.getPrivate(),
password.toCharArray(), new Certificate[] { cert });
out = new ByteArrayOutputStream();
cert.verify(keyPair.getPublic());
keyStore.store(out, password.toCharArray());
byte[] keyStoreData = out.toByteArray();
result.put("keyStoreData", keyStoreData);
return result;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
}
}
}
return result;
}
/**
* @param issuerStr
* @param subjectStr
* @param keyPair
* @param result
* @param certificateCRL
* @param extensions
* @return
*/
public static Certificate generateCertificateV3(String issuerStr,
String subjectStr, KeyPair keyPair, Map<String, byte[]> result,
String certificateCRL, List<Extension> extensions) {
ByteArrayInputStream bout = 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));
// privKey:使用自己的私鑰進行簽名,CA證書
if (extensions != null)
for (Extension ext : extensions) {
builder.addExtension(
new ASN1ObjectIdentifier(ext.getOid()),
ext.isCritical(),
ASN1Primitive.fromByteArray(ext.getValue()));
}
X509CertificateHolder holder = builder.build(singer);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
bout = new ByteArrayInputStream(holder.toASN1Structure()
.getEncoded());
cert = (X509Certificate) cf.generateCertificate(bout);
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 (bout != null) {
try {
bout.close();
} catch (IOException e) {
}
}
}
return cert;
}
public static void main(String[] args) throws Exception{
// CN: 名字與姓氏 OU : 組織單位名稱
// O :組織名稱 L : 城市或區域名稱 E : 電子郵件
// ST: 州或省份名稱 C: 單位的兩字母國家代碼
String issuerStr = "CN=電子簽名,OU=github,O=github,C=CN,L=西安,ST=陝西";
String subjectStr = "CN=電子簽名,OU=github,O=github,C=CN,L=西安,ST=陝西";
String certificateCRL = "https://syske.github.io/";
Map<String, byte[]> result = createCert("123456", issuerStr, subjectStr, certificateCRL);
FileOutputStream outPutStream = new FileOutputStream("D:/keystore.p12"); // ca.jks
outPutStream.write(result.get("keyStoreData"));
outPutStream.close();
FileOutputStream fos = new FileOutputStream(new File("D:/keystore.cer"));
fos.write(result.get("certificateData"));
fos.flush();
fos.close();
}
}
package io.github.syske.common.util;
/**
* @program: dpf-sign-test
* @description:
* @author: syske
* @create: 2019-12-04 09:00
*/
public class Extension {
private String oid;
private boolean critical;
private byte[] value;
public String getOid() {
return oid;
}
public byte[] getValue() {
return value;
}
public boolean isCritical() {
return critical;
}
}
生成簽名
生成的簽名是png圖片
package io.github.syske.common.util;
/**
* @program: dpf-sign-test
* @description:
* @author: syske
* @create: 2019-12-03 22:50
*/
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import sun.font.FontDesignMetrics;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
public class SignImage {
/**
* @param doctorName String 醫生名字
* @param hospitalName String 醫生名稱
* @param date String 簽名日期
* 圖片高度
* @param jpgname String jpg圖片名
* @return
*/
public static boolean createSignTextImg(
String doctorName, //
String hospitalName, //
String date,
String jpgname) {
int width = 255;
int height = 100;
FileOutputStream out = null;
//背景色
Color bgcolor = Color.WHITE;
//字色
Color fontcolor = Color.RED;
Font doctorNameFont = new Font(null, Font.BOLD, 20);
Font othorTextFont = new Font(null, Font.BOLD, 18);
try { // 寬度 高度
BufferedImage bimage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
Graphics2D g = bimage.createGraphics();
g.setColor(bgcolor); // 背景色
g.fillRect(0, 0, width, height); // 畫一個矩形
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); // 去除鋸齒(當設置的字體過大的時候,會出現鋸齒)
g.setColor(Color.RED);
g.fillRect(0, 0, 8, height);
g.fillRect(0, 0, width, 8);
g.fillRect(0, height - 8, width, height);
g.fillRect(width - 8, 0, width, height);
g.setColor(fontcolor); // 字的顏色
g.setFont(doctorNameFont); // 字體字形字號
FontMetrics fm = FontDesignMetrics.getMetrics(doctorNameFont);
int font1_Hight = fm.getHeight();
int strWidth = fm.stringWidth(doctorName);
int y = 35;
int x = (width - strWidth) / 2;
g.drawString(doctorName, x, y); // 在指定坐標除添加文字
g.setFont(othorTextFont); // 字體字形字號
fm = FontDesignMetrics.getMetrics(othorTextFont);
int font2_Hight = fm.getHeight();
strWidth = fm.stringWidth(hospitalName);
x = (width - strWidth) / 2;
g.drawString(hospitalName, x, y + font1_Hight); // 在指定坐標除添加文字
strWidth = fm.stringWidth(date);
x = (width - strWidth) / 2;
g.drawString(date, x, y + font1_Hight + font2_Hight); // 在指定坐標除添加文字
g.dispose();
out = new FileOutputStream(jpgname); // 指定輸出文件
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(bimage);
param.setQuality(50f, true);
encoder.encode(bimage, param); // 存盤
out.flush();
return true;
} catch (Exception e) {
return false;
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
}
}
}
}
public static void main(String[] args) {
createSignTextImg("華佗", "在線醫院", "2018.01.01", "sign.jpg");
}
}
簽名實現
這里需要注意的是,簽名入參中的頁碼,如果是最后一頁,頁碼傳null
package io.github.syske.common.util;
import com.itextpdf.awt.geom.Rectangle2D;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfSignatureAppearance.RenderingMode;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.itextpdf.text.pdf.security.*;
import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
import org.apache.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.UUID;
/**
* @program: SignPdf
* @description:
* @author: syske
* @create: 2019-12-03 22:54
*/
public class SignPdf {
private static Logger logger = Logger.getLogger(SignPdf.class);
private static final String PASSWORD = "123456"; // 秘鑰密碼
private static final String KEY_STORE_PATH = "d:\\keystore.p12"; // 秘鑰文件路徑
private SignPdf() {
}
/**
* 圖片簽章,指定簽名坐標位置
*
* @param signPdfSrc
* 簽名的PDF文件
* @param signedPdfOutFile
* 簽名后的的PDF文件
* @param signImage
* 簽名圖片完整路徑
* @param x
* 以左下角為原點x坐標值
* @param y
* 以左下角為原點Y坐標值
* @param numberOfPages
* 簽名頁碼,如果是最后一頁則傳null
* @param pageStyle
* 頁面布局,橫向或者縱向
* @throws Exception
*/
public static void sign(String signPdfSrc, String signedPdfOutFile,
String signImage, Float x, Float y, Integer numberOfPages,
PageStyle pageStyle) throws Exception {
sign(signPdfSrc, signedPdfOutFile, signImage, x, y, null,
numberOfPages, pageStyle);
}
/**
* 圖片簽章,指定關鍵字
*
* @param signPdfSrc
* 簽名的PDF文件
* @param signedPdfFile
* 簽名后的的PDF文件
* @param signImage
* 簽名圖片完整路徑
* @param keyWords
* 關鍵字
* @param numberOfPages
* 簽名頁碼,如果是最后一頁則傳null
* @param pageStyle
* 頁面布局,橫向或者縱向
*/
public static void sign(String signPdfSrc, String signedPdfFile,
String signImage, String keyWords, Integer numberOfPages,
PageStyle pageStyle) throws Exception {
sign(signPdfSrc, signedPdfFile, signImage, null, null, keyWords,
numberOfPages, pageStyle);
}
/**
* 私人簽章
*
* @param signPdfSrc
* 簽名的PDF文件
* @param signedPdfOutFile
* 簽名后的的PDF文件
* @param signImage
* 簽名圖片完整路徑
* @param x
* 以左下角為原點x坐標
* @param y
* 以左下角為原點y坐標
* @param keyWords
* 關鍵字
* @param numberOfPages
* 簽名頁碼,如果是最后一頁則傳null
* @param pageStyle
* 頁面布局,橫向或者縱向
* @return
*/
public static void sign(String signPdfSrc, String signedPdfOutFile,
String signImage, Float x, Float y, String keyWords,
Integer numberOfPages, PageStyle pageStyle) throws Exception {
File signPdfSrcFile = new File(signPdfSrc);
PdfReader reader = null;
ByteArrayOutputStream signPDFData = null;
PdfStamper stp = null;
FileInputStream fos = null;
FileOutputStream pdfOutputStream = null;
try {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
KeyStore ks = KeyStore.getInstance("PKCS12",
new BouncyCastleProvider());
fos = new FileInputStream(KEY_STORE_PATH);
// 私鑰密碼 為Pkcs生成證書是的私鑰密碼 123456
ks.load(fos, PASSWORD.toCharArray());
String alias = ks.aliases().nextElement();
PrivateKey key = (PrivateKey) ks.getKey(alias,
PASSWORD.toCharArray());
Certificate[] chain = ks.getCertificateChain(alias);
reader = new PdfReader(signPdfSrc); // 也可以輸入流的方式構建
signPDFData = new ByteArrayOutputStream();
numberOfPages = numberOfPages == null ? reader.getNumberOfPages()
: 0;
// 臨時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(RenderingMode.GRAPHIC);
float llx = 0f;
float lly = 0f;
float signImageWidth = image.getWidth();
float signImageHeight = image.getHeight();
float signImageHeightSocale = 85 / signImageWidth * signImageHeight;
if (keyWords != null && !keyWords.isEmpty()) {
KeyWordInfo keyWordInfo = getKeyWordLocation(numberOfPages,
keyWords, reader);
Rectangle pageSize = reader.getPageSize(numberOfPages);
float width = pageSize.getWidth();
if (PageStyle.PAGE_STYLE_LANDSCAPE.equals(pageStyle)) {
llx = keyWordInfo.getY() + (float) keyWordInfo.getHeight();
lly = width - keyWordInfo.getX() - signImageHeightSocale
/ 2;
} else if (PageStyle.PAGE_STYLE_PORTRAIT.equals(pageStyle)) {
llx = keyWordInfo.getX() + (float) keyWordInfo.getWidth()*keyWords.length();
lly = keyWordInfo.getY() - signImageHeightSocale / 2;
}
} else if (x != null && y != null) {
llx = x;
lly = y;
} else {
throw new Exception("坐標和關鍵字不能同時為空!");
}
float urx = llx + 85;
float ury = lly + signImageHeightSocale;
// 是對應x軸和y軸坐標
sap.setVisibleSignature(new Rectangle(llx, lly, urx, ury),
numberOfPages,
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, CryptoStandard.CADES);
stp.close();
reader.close();
pdfOutputStream = new FileOutputStream(signedPdfOutFile);
pdfOutputStream.write(signPDFData.toByteArray());
pdfOutputStream.close();
} catch (KeyStoreException e) {
logger.error("簽名驗證失敗", e);
throw new Exception("簽名驗證失敗", e);
} catch (FileNotFoundException e) {
logger.error("文件未找到", e);
throw new Exception("文件未找到", e);
} catch (IOException e) {
logger.error("IO異常", e);
throw new Exception("IO異常", e);
} catch (Exception e) {
logger.error("簽章失敗", e);
throw new Exception("簽章失敗", e);
} finally {
if (signPDFData != null) {
try {
signPDFData.close();
} catch (IOException e) {
logger.error("資源關閉失敗", e);
throw new Exception("資源關閉失敗", e);
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
logger.error("資源關閉失敗", e);
throw new Exception("資源關閉失敗", e);
}
}
if (pdfOutputStream != null) {
try {
pdfOutputStream.close();
} catch (IOException e) {
logger.error("資源關閉失敗", e);
throw new Exception("資源關閉失敗", e);
}
}
}
}
/**
* 查找關鍵字定位
*
* @param numberOfPages
* @param keyWords
* 關鍵字
* @param reader
* @return
* @throws IOException
*/
private static KeyWordInfo getKeyWordLocation(Integer numberOfPages,
final String keyWords, PdfReader reader) throws IOException {
PdfReaderContentParser pdfReaderContentParser = new PdfReaderContentParser(
reader);
final KeyWordInfo keyWordInfo = new KeyWordInfo();
pdfReaderContentParser.processContent(numberOfPages,
new RenderListener() {
@Override
public void renderText(TextRenderInfo textRenderInfo) {
String text = textRenderInfo.getText(); // 整頁內容
if (null != text && text.contains(keyWords)) {
Rectangle2D.Float boundingRectange = textRenderInfo
.getBaseline().getBoundingRectange();
float leftY = (float) boundingRectange.getMinY() - 1;
float rightY = (float) boundingRectange.getMaxY() + 1;
logger.debug(boundingRectange.x + "--"
+ boundingRectange.y + "---");
keyWordInfo.setHeight(rightY - leftY);
keyWordInfo.setWidth((rightY - leftY)
* keyWords.length());
keyWordInfo.setX(boundingRectange.x);
keyWordInfo.setY(boundingRectange.y);
}
}
@Override
public void renderImage(ImageRenderInfo arg0) {}
@Override
public void endTextBlock() {}
@Override
public void beginTextBlock() {}
});
return keyWordInfo;
}
private static class KeyWordInfo {
private float x;
private float y;
private double width;
private double height;
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
}
enum PageStyle {
PAGE_STYLE_LANDSCAPE, // 橫向
PAGE_STYLE_PORTRAIT // 縱向
}
public static PageStyle getPageStyle_LANDSCAPE() {
return PageStyle.PAGE_STYLE_LANDSCAPE;
}
public static PageStyle getPageStyle_PORTRAIT() {
return PageStyle.PAGE_STYLE_PORTRAIT;
}
public static void main(String[] args) throws Exception {
sign("E:\\test2_sgin.pdf",//
"D:\\signed-35.pdf",
"E:\\workSpeace\\pansky\\other_files\\電子簽章\\png\\sign_0000_1_yxz.png",
null, null, "負責人", null, PageStyle.PAGE_STYLE_LANDSCAPE);
// read();
// test();
}
}
項目pom依賴
差點都忘記了😂
<?xml version="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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.syske</groupId>
<artifactId>dpf-sign-test</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf/itext-asian -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.64</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
</project>
簽名預覽:
總結
因為本身業務不復雜,所以也沒有過多的說明,但以上代碼實現部分還有很多需要改進的地方,比如:
1、查找關鍵字的時候,只獲取了一次坐標,但是同一頁關鍵字可能存在多個,如果想在相同關鍵字的地方都蓋章,是不能實現的,如果你正好有這樣的需求,你可以動手自己改造;
2、和第一個類似,沒有實現全文檔簽名