历经一个月总结使用java实现pdf文件的电子签字+盖章+防伪二维码+水印+PDF文件加密的全套解决方案
一、前言
九月中旬到十月底,我和同事参加了某个系统的开发,涉及到对PDF的电子签字+盖章+防伪二维码+水印等,我最开始选择使用pageoffice实现PDF的盖章和签字,并且也写了一篇博客来进行详细的介绍(pageoffice实现签名盖章:http://t.csdn.cn/nNxpe),但是出现一个问题,PageOffice支持JAVA、ASP.NET、PHP多种编程开发语言,使开发集成简单高效,事半功倍。让集成PAGEOFFICE的协同办公系统更具价值,但是他是卓正软件公司的一个项目,需要收费,并且安全系数不一定能得到保证,而这个项目需要上生产,所以经过多方的研究,学习,总结,今天终于将一套PDF集成线上签字+盖章+防伪二维码+水印的一系列解决方法总结出来,今天写这篇博客进行开源
二、使用itextPDF实现PDF电子公章工具类
1、电子公章的制作
我们需要实现电子公章盖章,但是不能使用公司的章,我们这次推荐使用的线上做章工具来模拟电子印章
做章网站:http://seal.biaozhiku.com/
我们选择圆形印章
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_1ySdjiVqeM.png)
然后输入公司名,输入章名输入编码然后点击395生成,最后点击保存图片,我们的个人专业章就实现了
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_lr-LPzZxXi.png)
电子公章效果如图:
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%B0%8F%E7%8E%8B%E5%B7%A5%E4%BD%9C%E5%AE%A4_seal.biaozhiku.com_alpha_hJzMkdI_mm.png)
PDF模板图
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%B0%8F%E7%8E%8B_00_s58T3b8wlh.jpg)
生成PDF效果图
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/demo3_00_255Cxa_eGz.jpg)
2、itextPDF的相关依赖
com.itextpdf
itextpdf
5.5.10
com.itextpdf
itext-asian
5.2.0
org.bouncycastle
bcprov-jdk15on
1.49
org.bouncycastle
bcpkix-jdk15on
1.49
使用的是boot项目,所以完整依赖是
org.springframework.boot
spring-boot-starter-parent
2.2.5.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
com.itextpdf
itextpdf
5.5.10
com.itextpdf
itext-asian
5.2.0
org.bouncycastle
bcprov-jdk15on
1.49
org.bouncycastle
bcpkix-jdk15on
1.49
3、相关配置及数字签名的配置
(1)摘要算法
我们项目启动之后报错
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_23PdD597Lv.png)
需要加这个配置文件就不报错了,这个主要原因是摘要算法没有,需要引入相关依赖
org.bouncycastle
bcprov-jdk15on
1.49
org.bouncycastle
bcpkix-jdk15on
1.49
涉及到加密算法就需要数字签名了(数字签名格式,CMS,CADE),我们就需要一个文件(我的命名是:server.p12)这个东西需要我们自己电脑生成数字签名
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_qzl-QVWTIi.png)
(2)java工具keytool生成p12数字证书文件
Keytool是用于管理**和证书的工具,位于%JAVA_HOME%/bin目录。
使用JDK的keytool工具
keytool在jdk的bin目录下
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_A5L2e9Fqso.png)
2. 打开keytool所在的bin目录,然后在上面的路径显示框中输入CMD,然后回车,即可在当前文件夹下打开命令提示符,并且路径是当前文件夹。
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_KgdNXg9mE6.png)
生成数字文件,在命令行输入
keytool -genkeypair -alias whj -keypass 111111 -storepass 111111 -dname “C=CN,ST=SD,L=QD,O=haier,OU=dev,CN=haier.com” -keyalg RSA -keysize 2048 -validity 3650 -keystore D:\keystore\server.keystore
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_cynvPl3LoN.png)
参数解释:
storepass keystore 文件存储密码
keypass 私钥加解密密码
alias 实体别名(包括证书私钥)
dname 证书个人信息
keyalt 采用公钥算法,默认是DSA keysize **长度(DSA算法对应的默认算法是sha1withDSA,不支持2048长度,此时需指定RSA)
validity 有效期
keystore 指定keystore文件
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_ZWjJHgBwtp.png)
转换为p12格式
在命令行输入
keytool -importkeystore -srckeystore D:\keystore\server.keystore -destkeystore D:\keystore\whj.p12 -srcalias whj -destalias serverkey -srcstoretype jks -deststoretype pkcs12 -srcstorepass 111111 -deststorepass 111111 -noprompt
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image__ik2dGnUma.png)
生成的最终文件
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_PQAR_RUAi-.png)
4、项目结构及源码
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_t3aI1iEX1r.png)
工具类(可以直接复制)
package com.whj.pdf;
import com.itextpdf.text.DocumentException;
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.PdfStamper;
import com.itextpdf.text.pdf.security.*;
import com.whj.entity.SignatureInfo;
import java.io.*;
import java.security.GeneralSecurityException;
/**
* @author 王恒杰
* @date 2022/10/13 22:52
* @Description:盖章功能工具类
*/
public class ItextUtil {
public static final char[] PASSWORD = "111111".toCharArray();// keystory密码
/**
* 单多次签章通用
*
* @param src
* @param target
* @param signatureInfo
* @throws GeneralSecurityException
* @throws IOException
* @throws DocumentException
*/
@SuppressWarnings("resource")
public void sign(String src, String target, SignatureInfo signatureInfo) {
InputStream inputStream = null;
FileOutputStream outputStream = null;
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
inputStream = new FileInputStream(src);
ByteArrayOutputStream tempArrayOutputStream = new ByteArrayOutputStream();
PdfReader reader = new PdfReader(inputStream);
// 创建签章工具PdfStamper ,最后一个boolean参数是否允许被追加签名
// false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
// true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
PdfStamper stamper = PdfStamper.createSignature(reader,
tempArrayOutputStream, '\0', null, true);
// 获取数字签章属性对象
PdfSignatureAppearance appearance = stamper
.getSignatureAppearance();
appearance.setReason(signatureInfo.getReason());
appearance.setLocation(signatureInfo.getLocation());
// 设置签名的位置,页码,签名域名称,多次追加签名的时候,签名预名称不能一样 图片大小受表单域大小影响(过小导致压缩)
// 签名的位置,是图章相对于pdf页面的位置坐标,原点为pdf页面左下角
// 四个参数的分别是,图章左下角x,图章左下角y,图章右上角x,图章右上角y
//四个参数的分别是,图章左下角x,图章左下角y,图章右上角x,图章右上角y
appearance.setVisibleSignature(new Rectangle(280, 220, 140, 600), 1, "sig1");
// 读取图章图片
Image image = Image.getInstance(signatureInfo.getImagePath());
appearance.setSignatureGraphic(image);
appearance.setCertificationLevel(signatureInfo
.getCertificationLevel());
// 设置图章的显示方式,如下选择的是只显示图章(还有其他的模式,可以图章和签名描述一同显示)
appearance.setRenderingMode(signatureInfo.getRenderingMode());
// 这里的itext提供了2个用于签名的接口,可以自己实现,后边着重说这个实现
// 摘要算法
ExternalDigest digest = new BouncyCastleDigest();
// 签名算法
ExternalSignature signature = new PrivateKeySignature(
signatureInfo.getPk(), signatureInfo.getDigestAlgorithm(),
null);
// 调用itext签名方法完成pdf签章 //数字签名格式,CMS,CADE
MakeSignature.signDetached(appearance, digest, signature,
signatureInfo.getChain(), null, null, null, 0,
MakeSignature.CryptoStandard.CADES);
inputStream = new ByteArrayInputStream(
tempArrayOutputStream.toByteArray());
// 定义输入流为生成的输出流内容,以完成多次签章的过程
result = tempArrayOutputStream;
outputStream = new FileOutputStream(new File(target));
outputStream.write(result.toByteArray());
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != outputStream) {
outputStream.close();
}
if (null != inputStream) {
inputStream.close();
}
if (null != result) {
result.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
实体类
package com.whj.entity;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import java.security.PrivateKey;
import java.security.cert.Certificate;
/**
* @author 王恒杰
* @date 2022/10/13 22:52
* @Description:
*/
public class SignatureInfo {
private String reason; //签名的原因,显示在pdf签名属性中
private String location;//签名的地点,显示在pdf签名属性中
private String digestAlgorithm;//摘要算法名称,例如SHA-1
private String imagePath;//图章路径
private String fieldName;//表单域名称
private Certificate[] chain;//证书链
private PrivateKey pk;//签名私钥
private int certificationLevel = 0; //批准签章
private PdfSignatureAppearance.RenderingMode renderingMode;//表现形式:仅描述,仅图片,图片和描述,签章者和描述
//图章属性
private float rectllx;//图章左下角x
private float rectlly;//图章左下角y
private float recturx;//图章右上角x
private float rectury;//图章右上角y
public float getRectllx() {
return rectllx;
}
public void setRectllx(float rectllx) {
this.rectllx = rectllx;
}
public float getRectlly() {
return rectlly;
}
public void setRectlly(float rectlly) {
this.rectlly = rectlly;
}
public float getRecturx() {
return recturx;
}
public void setRecturx(float recturx) {
this.recturx = recturx;
}
public float getRectury() {
return rectury;
}
public void setRectury(float rectury) {
this.rectury = rectury;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getDigestAlgorithm() {
return digestAlgorithm;
}
public void setDigestAlgorithm(String digestAlgorithm) {
this.digestAlgorithm = digestAlgorithm;
}
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public String getFieldName() {
return fieldName;
}
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
public Certificate[] getChain() {
return chain;
}
public void setChain(Certificate[] chain) {
this.chain = chain;
}
public PrivateKey getPk() {
return pk;
}
public void setPk(PrivateKey pk) {
this.pk = pk;
}
public int getCertificationLevel() {
return certificationLevel;
}
public void setCertificationLevel(int certificationLevel) {
this.certificationLevel = certificationLevel;
}
public PdfSignatureAppearance.RenderingMode getRenderingMode() {
return renderingMode;
}
public void setRenderingMode(PdfSignatureAppearance.RenderingMode renderingMode) {
this.renderingMode = renderingMode;
}
}
测试
package com.whj.pdf;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.security.DigestAlgorithms;
import com.whj.entity.SignatureInfo;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import static com.whj.pdf.ItextUtil.PASSWORD;
/**
* @author 王恒杰
* @date 2022/10/24 10:42
* @Description: 盖章功能实现
*/
public class PdfStamp {
public static void main(String[] args) {
try {
ItextUtil app = new ItextUtil();
// 将证书文件放入指定路径,并读取keystore ,获得私钥和证书链
String pkPath = "src/main/resources/whj.p12";
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream(pkPath), PASSWORD);
String alias = ks.aliases().nextElement();
PrivateKey pk = (PrivateKey) ks.getKey(alias, PASSWORD);
// 得到证书链
Certificate[] chain = ks.getCertificateChain(alias);
//需要进行签章的pdf
String path = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\程序员小王.pdf";
// 封装签章信息
SignatureInfo signInfo = new SignatureInfo();
signInfo.setReason("理由");
signInfo.setLocation("位置");
signInfo.setPk(pk);
signInfo.setChain(chain);
signInfo.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
signInfo.setDigestAlgorithm(DigestAlgorithms.SHA1);
signInfo.setFieldName("demo");
// 签章图片
signInfo.setImagePath("D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\chapter.png");
signInfo.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
//// 值越大,代表向x轴坐标平移 缩小 (反之,值越小,印章会放大)
signInfo.setRectllx(100);
//// 值越大,代表向y轴坐标向上平移(大小不变)
signInfo.setRectlly(200);
// 值越大 代表向x轴坐标向右平移 (大小不变)
signInfo.setRecturx(150);
// 值越大,代表向y轴坐标向上平移(大小不变)
signInfo.setRectury(150);
//签章后的pdf路径
app.sign(path, "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\out.pdf", signInfo);
} catch (Exception e) {
e.printStackTrace();
}
}
}
5、结果展示
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_UnvRIku7C9.png)
三、thymeleaf+itext签字功能+PDF文件加密实现
所以需依赖
需要手工导入一个jar包(jar包我放在了源码里面,需要自行下载)
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_DLEtxY_wX1.png)
org.springframework.boot
spring-boot-starter-parent
2.2.5.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
com.itextpdf
itextpdf
5.5.10
com.itextpdf
itext-asian
5.2.0
org.bouncycastle
bcprov-jdk15on
1.49
org.bouncycastle
bcpkix-jdk15on
1.49
com.google.guava
guava
25.0-jre
因为这个的相关类太多,只展示了核心代码,全部代码请到博客底部下载源码
![](https://csdn-image.oss-cn-beijing.aliyuncs.com/img/image_twmNnIa3Ms.png)
1、签字图片上传和签字实现的Controller
package com.whj.controller;
import com.whj.service.SignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author 王恒杰
* @date 2022/10/24 13:57
* @Description: 实现签字上传和签字功能
*/
@RestController
@RequestMapping("/sign")
public class SignController {
@Autowired
private SignService signService;
@PostMapping(value = "/uploadSign")
@ResponseBody
public String uploadSign(String img) {
return signService.uploadSign(img);
}
@PostMapping(value = "/sign")
@ResponseBody
public String sign() {
return signService.sign(id);
}
}
2、签字上传,签字,PDF文件加密业务层实现
/**
* @author 王恒杰
* @date 2022/10/24 14:00
* @Description:
*/
@Service
public class SignServiceImpl implements SignService{
@Override
public String uploadSign(String img) {
//String idCard="3000";
// 生成jpeg图片 idCard.asString()
String url = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\img\\sign.png";
try {
if (img == null) // 图像数据为空
{
return "no";
}
int i = img.indexOf("base64,") + 7;//获取前缀data:image/gif;base64,的坐标
String newImage = img.substring(i, img.length());//去除前缀
BASE64Decoder decoder = new BASE64Decoder();
// Base64解码
byte[] bytes = decoder.decodeBuffer(newImage);
for (int j = 0; j 0) {
point.bx = point.x;
point.by = point.y;
point.x = clickX.pop();
point.y = clickY.pop();
console.log(point.x);
console.log(point.y);
/*alert(point.x);
alert(point.y);*/
point.drag = clickDrag.pop();
context.beginPath();
if(point.drag && point.notFirst) {
context.moveTo(point.bx, point.by);
} else {
point.notFirst = true;
context.moveTo(point.x - 1, point.y);
}
context.lineTo(point.x, point.y);
context.closePath();
context.stroke();
}
for(var i=0; i |