1 说明
本文主要讲解使用Java和SpringBoot框架设计实现对PDF的签章操作。实现对PDF的签章操作不是简单的找个图片贴到PDF上即可,而是需要申请数字证书才能对PDF签章,否则无法验证签章的身份。具体实现分两步进行,第一步生成PKCS12证书,第二步添加签章。
2 生成PKCS12证书
生成PKCS12证书是通过使用bouncycastle包实现的,具体实现如下所示,首先引入依赖。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.49</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.49</version>
</dependency>
步骤1: 编写生成PKCS12证书工具类
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
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 com.css.modules.file.entity.Extension;
/**
* 证书生成工具类
*
* @author 一朝风月
* @date 2021/8/6 14:20
*/
public class GenerateCertificateUtil {
private static KeyPair getKey() throws NoSuchAlgorithmException {
// 密钥对 生成器,RSA算法 生成的 提供者是 BouncyCastle
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
// 密钥长度 1024
generator.initialize(1024);
// 证书中的密钥 公钥和私钥
return generator.generateKeyPair();
}
/**
* 创建证书
*
* @param password 密码
* @param issuerStr 颁发机构信息
* @param subjectStr 使用者信息
* @param certificateCrl 颁发地址
* @return 证书
*/
public static Map<String, byte[]> createCert(String password, String issuerStr, String subjectStr,
String certificateCrl, List<Extension> extensions) {
Map<String, byte[]> result = new HashMap<>();
ByteArrayOutputStream out = null;
try {
// 生成证书
// KeyStore keyStore = KeyStore.getInstance("JKS");
KeyStore keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
keyStore.load(null, null);
KeyPair keyPair = getKey();
// issuer与 subject相同的证书就是CA证书
Certificate cert = generateCertificate(issuerStr, subjectStr, keyPair, result, certificateCrl, extensions);
// 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 {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
/**
* 生成证书
*
* @param issuerStr 事项字符串
* @param subjectStr 主题字符串
* @param keyPair 密钥对(公钥,私钥)
* @param result 结果
* @param certificateCrl CRL分发点
* @param extensions 扩展字段
* @return 生成证书
*/
private static Certificate generateCertificate(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 identifier = 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(identifier, 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));
// privateKey:使用自己的私钥进行签名,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(StandardCharsets.UTF_8));
//证书有效结束时间
result.put("notAfter", format.format(notAfter).getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bout != null) {
try {
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return cert;
}
}
步骤2: 证书参数类
import lombok.Data;
/**
* 证书参数
*
* @author 一朝风月
* @date 2021/8/6 16:06
*/
@Data
public class CertificateParams {
/**
* 事项字符串
*/
private String issuerStr;
/**
* 主题字符串
*/
private String subjectStr;
/**
* CRL分发点
*/
private String certificateCrl;
/**
* 证书密码
*/
private String password;
/**
* 额外的信息
*/
private String extensionsJson;
}
步骤3: 编写获取证书接口
/**
* 获取PKCS12证书
*
* @param params 证书参数
* @param response HTTP响应
* @return PKCS12证书
* @throws IOException 异常
*/
@PostMapping("getCertificate")
public Result mGetCertificate(CertificateParams params, HttpServletResponse response) throws IOException {
/*
* 填写说明
*
* CN: 名字与姓氏 OU : 组织单位名称
* O :组织名称 L : 城市或区域名称 E : 电子邮件
* ST: 州或省份名称 C: 单位的两字母国-家代码
*
* 例如:
* String issuerStr = "CN=jcb凭证,OU=研发部,O=jcb有限公司,C=CN,E=jcb@sina.com,L=北京,ST=北京";
* String subjectStr = "CN=jcb有限公司,OU=用户,O=test,C=CN,E=jcb@sina.com,L=北京,ST=北京";
* String certificateCrl = "https://jcb.cn";
*/
Map<String, byte[]> result = GenerateCertificateUtil.createCert(params.getPassword(), params.getIssuerStr(),
params.getSubjectStr(), params.getCertificateCrl(),
JSONArray.parseArray(params.getExtensionsJson(), Extension.class));
// 将证书放置在response中
response.setContentType("application/*");
response.setHeader("Content-Disposition", "attachment;filename=\"" + "PKCS12证书" + "\"");
IOUtils.write(result.get("keyStoreData"), response.getOutputStream());
return null;
}
3 使用证书添加签章
使用刚刚生成的证书添加签章,依赖如下所示:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
3.1 签章参数类
import lombok.Data;
/**
* 签章参数
*
* @author 一朝风月
* @date 2021/8/6 10:12
*/
@Data
public class SignSealParams {
/**
* 原因
*/
private String reason;
/**
* 位置
*/
private String location;
/**
* 页码
*/
private int pageNum;
/**
* 签名域名称,多次追加签名的时候,签名域名称不能一样
*/
private String fieldName;
/**
* 证书密码
*/
private String password;
/**
* 图章左下角x
*/
private int llx;
/**
* 图章左下角y
*/
private int lly;
/**
* 图章右上角x
*/
private int urx;
/**
* 图章右上角y
*/
private int ury;
SignSealParams() {
this.pageNum = 1;
this.fieldName = "sig1";
this.llx = 200;
this.lly = 200;
this.urx = 300;
this.ury = 300;
}
}
3.2 添加签章
/**
* 添加签章
*
* @param file PDF
* @param seal 印章或者签名
* @param certificate PKCS12证书
* @param params 参数
* @param response HTTP响应
* @return 返回增加签章的PDF
* @throws Exception 异常
*/
@PostMapping("addSeal")
public Result mAddSeal(MultipartFile file, MultipartFile seal, MultipartFile certificate,
SignSealParams params, HttpServletResponse response) throws Exception {
FileProps fileProps = new FileProps(file);
if (!"pdf".equalsIgnoreCase(fileProps.getFileType())) {
return Result.error("请传入PDF文件!");
}
PdfReader reader = new PdfReader(file.getInputStream());
response.setContentType("application/*");
response.setHeader("Content-Disposition", "attachment;filename=\"" + fileProps.getFileName() + "\"");
/*
* 创建签章工具PdfStamper 最后一个boolean参数,
* false:pdf文件只允许被签名一次,多次签名,最后一次有效
* true:pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
*/
PdfStamper stamper = PdfStamper.createSignature(reader, response.getOutputStream(),
'\0', null, true);
// 获取数字签章属性对象,设定数字签章的属性
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setReason(params.getReason());
appearance.setLocation(params.getLocation());
appearance.setVisibleSignature(
new Rectangle(params.getLlx(), params.getLly(), params.getUrx(), params.getUry()),
params.getPageNum(), params.getFieldName());
Image image = Image.getInstance(seal.getBytes());
appearance.setSignatureGraphic(image);
appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
ExternalDigest digest = new BouncyCastleDigest();
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certificate.getInputStream(), params.getPassword().toCharArray());
String alias = ks.aliases().nextElement();
PrivateKey pk = (PrivateKey) ks.getKey(alias, params.getPassword().toCharArray());
Certificate[] chain = ks.getCertificateChain(alias);
ExternalSignature signature = new PrivateKeySignature(pk, DigestAlgorithms.SHA1, null);
MakeSignature.signDetached(appearance, digest, signature, chain, null,
null, null, 0, CryptoStandard.CMS);
return null;
}
4 结果
测试一下上述代码的执行结果