何为pdf签章
有的时候为了保证自己pdf文件的版权,或者隐私。需要在pdf中进行签章(盖章);注意签章不是简单的将一张印章图片贴在pdf中。而是将印章的信息(证书+证书图片)放在pdf中,看起来是盖了一个印章图片,但是可以鼠标点击印章显示印章中包含的证书信息(这个功能需要专业的pdf软件,如:Adobe reader PDF);
依赖
除了pdfbox基础的那几个jar(pdfbox、fontbox、xmpbox),还需要引入app
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-app</artifactId>
<version>2.0.20</version>
</dependency>
上代码
印章入参实体
import lombok.Data;
import java.io.InputStream;
/**
* @author Andy
*/
@Data
public class SignatureInfo {
/**
* 需要签章的pdf文件输入流
*/
private InputStream signPdfInputStream;
/**
* 证书输入流
*/
private InputStream certificateInputStream;
/**
* 证书密码
*/
private String password;
/**
* 印章附加信息(理由、地址等...)
*/
private SignatureAppearance signatureAppearance;
/**
* 印章图片输入流
*/
private InputStream imageInputStream;
/**
* 图章x坐标
*/
private float rectllX;
/**
* 图章Y坐标
*/
private float rectllY;
/**
* 印章宽度
*/
private float imageWidth;
/**
* 印章高度
*/
private float imageHight;
/**
* 在第几页签名
*/
private Integer pageNo = 1;
/**
* 摘要算法名称
*/
private String signatureAlgorithm;
}
签章信息
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.security.KeyStore;
import java.security.Provider;
/**
* @author Andy
*/
@Data
@ApiModel("签章信息")
public class SignatureCryptoInfo {
private Provider provider;
private KeyStore keystore;
private char[] password;
private String certAlias;
}
印章附加信息
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author Andy
*/
@Data
@ApiModel("印章附加信息")
public class SignatureAppearance {
private String reason;
private String contact;
private String location;
@ApiModelProperty("是否可见")
private boolean visibleSignature = true;
}
加密算法实体
public class SignatureAlgorithm {
public static final String SHA1 = "SHA1withRSA";
public static final String SHA256 = "SHA256withRSA";
public static final String SH384 = "SHA384withRSA";
public static final String SHA512 = "SHA512withRSA";
public static final String RIPEMD = "RIPEMDwithRSA";
}
签章工具类
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.*;
import java.security.cert.CRLException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Hashtable;
import java.util.List;
import com.zmg.panda.utils.pdfbox.sign.bean.SignatureAppearance;
import com.zmg.panda.utils.pdfbox.sign.bean.SignatureCryptoInfo;
import com.zmg.panda.utils.pdfbox.sign.bean.SignatureInfo;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cert.jcajce.JcaX509CRLHolder;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* @author Andy
*/
public class PdfboxSignManager implements SignatureInterface {
/**
* 私钥
*/
private PrivateKey privateKey;
/**
* 证书链
*/
private Certificate[] certChain;
/**
* 摘要算法名称
*/
private String signatureAlgorithm;
public PdfboxSignManager(SignatureInfo signatureInfo) {
try {
// 密码
char[] password = signatureInfo.getPassword().toCharArray();
// 得到证书链
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(signatureInfo.getCertificateInputStream(), password);
String certAlias = keyStore.aliases().nextElement();
certChain = keyStore.getCertificateChain(certAlias);
// 私钥
privateKey = (PrivateKey) keyStore.getKey(certAlias, password);
signatureAlgorithm = signatureInfo.getSignatureAlgorithm();
} catch (KeyStoreException e) {
throw new RuntimeException("pdf签章密码错误!", e);
} catch (UnrecoverableKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (CertificateException | IOException e) {
e.printStackTrace();
}
}
/**
* 给pdf签章
* @param signatureInfo
* @return
* @throws IOException
*/
public ByteArrayOutputStream signPDF(SignatureInfo signatureInfo) throws IOException {
Security.addProvider(new BouncyCastleProvider());
InputStream pdfInputStream = signatureInfo.getSignPdfInputStream();
SignatureAppearance signatureAppearance = signatureInfo.getSignatureAppearance();
if (pdfInputStream == null) {
throw new RuntimeException("找不到pdf文件");
}
// 加载pdf
PDDocument doc = PDDocument.load(pdfInputStream);
// 创建 签章dictionary
PDSignature signature = new PDSignature();
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
// 签章附加信息
signature.setName(signatureAppearance.getContact());
signature.setLocation(signatureAppearance.getLocation());
signature.setReason(signatureAppearance.getReason());
// 设置签名时间
signature.setSignDate(Calendar.getInstance());
// 注册签名字典和签名接口
SignatureOptions sigOpts;
if (signatureAppearance.isVisibleSignature()) {
sigOpts = createVisibleSignature(doc, signatureInfo);
} else {
sigOpts = new SignatureOptions();
}
sigOpts.setPreferredSignatureSize(100000);
doc.addSignature(signature, this, sigOpts);
// 输出
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
doc.saveIncremental(byteArrayOutputStream);
sigOpts.close();
return byteArrayOutputStream;
}
private SignatureOptions createVisibleSignature(PDDocument doc, SignatureInfo signatureInfo) throws IOException {
SignatureAppearance signatureAppearance = signatureInfo.getSignatureAppearance();
SignatureOptions sigOpts = new SignatureOptions();
PDVisibleSignDesigner visibleSig = new PDVisibleSignDesigner(doc, signatureInfo.getImageInputStream(), 1);
visibleSig.xAxis(signatureInfo.getRectllX())
.yAxis(signatureInfo.getRectllY())
.width(signatureInfo.getImageWidth())
.height(signatureInfo.getImageHight()).signatureFieldName("signature");
// 设置印章附加信息
PDVisibleSigProperties signatureProperties = new PDVisibleSigProperties();
signatureProperties
.preferredSize(0).page(1).visualSignEnabled(true)
.setPdVisibleSignature(visibleSig).buildSignature();
sigOpts.setVisualSignature(signatureProperties);
sigOpts.setPage(signatureInfo.getPageNo() - 1);
return sigOpts;
}
@Override
public byte[] sign(InputStream content) {
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
List<Certificate> certList = Arrays.asList(certChain);
try {
// SHA1 SHA256 SH384 SHA512
X509Certificate signingCert = (X509Certificate) certList.get(0);
gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC")
.setSignedAttributeGenerator(new AttributeTable(new Hashtable<>()))
.build(signatureAlgorithm, privateKey, signingCert));
gen.addCertificates(new JcaCertStore(certList));
X509CRL[] crls = fetchCRLs(signingCert);
for (X509CRL crl : crls) {
gen.addCRL(new JcaX509CRLHolder(crl));
}
CMSProcessableByteArray processable = new CMSProcessableByteArray(IOUtils.toByteArray(content));
CMSSignedData signedData = gen.generate(processable, false);
return signedData.getEncoded();
} catch (Exception e) {
throw new RuntimeException("Problem while preparing signature");
}
}
private X509CRL[] fetchCRLs(X509Certificate signingCert)
throws CertificateException, MalformedURLException, CRLException, IOException {
List<String> crlList = CRLDistributionPointsExtractor.getCrlDistributionPoints(signingCert);
List<X509CRL> crls = new ArrayList<X509CRL>();
for (String crlUrl : crlList) {
if (!crlUrl.startsWith("http")) {
continue;
}
CertificateFactory cf = CertificateFactory.getInstance("X.509");
URL url = new URL(crlUrl);
X509CRL crl = (X509CRL) cf.generateCRL(url.openStream());
crls.add(crl);
}
return crls.toArray(new X509CRL[] {});
}
}
CRL分发点管理器
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
/**
* @author Panda
*/
public class CRLDistributionPointsExtractor {
public static List<String> getCrlDistributionPoints(X509Certificate cert) {
ASN1InputStream oAsnInStream = null;
ASN1InputStream oAsnInStream2 = null;
try {
byte[] crldpExt = cert.getExtensionValue(Extension.cRLDistributionPoints.getId());
if (crldpExt == null) {
List<String> emptyList = new ArrayList<String>();
return emptyList;
}
oAsnInStream = new ASN1InputStream(new ByteArrayInputStream(crldpExt));
ASN1Primitive derObjCrlDP = oAsnInStream.readObject();
DEROctetString dosCrlDP = (DEROctetString) derObjCrlDP;
byte[] crldpExtOctets = dosCrlDP.getOctets();
oAsnInStream2 = new ASN1InputStream(new ByteArrayInputStream(crldpExtOctets));
ASN1Primitive derObj2 = oAsnInStream2.readObject();
CRLDistPoint distPoint = CRLDistPoint.getInstance(derObj2);
List<String> crlUrls = new ArrayList<String>();
for (DistributionPoint dp : distPoint.getDistributionPoints()) {
DistributionPointName dpn = dp.getDistributionPoint();
if (dpn != null) {
if (dpn.getType() == DistributionPointName.FULL_NAME) {
GeneralName[] genNames = GeneralNames.getInstance(dpn.getName()).getNames();
for (int j = 0; j < genNames.length; j++) {
if (genNames[j].getTagNo() == GeneralName.uniformResourceIdentifier) {
String url = DERIA5String.getInstance(genNames[j].getName()).getString();
crlUrls.add(url);
}
}
}
}
}
return crlUrls;
} catch (IOException ex) {
throw new RuntimeException(ex);
} finally {
IOUtils.closeQuietly(oAsnInStream);
IOUtils.closeQuietly(oAsnInStream2);
}
}
}
测试类
import com.zmg.panda.utils.pdfbox.sign.PdfboxSignManager;
import com.zmg.panda.utils.pdfbox.sign.bean.SignatureAlgorithm;
import com.zmg.panda.utils.pdfbox.sign.bean.SignatureAppearance;
import com.zmg.panda.utils.pdfbox.sign.bean.SignatureInfo;
import org.junit.Test;
import java.io.*;
public class SigningTest {
private static final String PDF_FILE_PATH = "D:\\tmp\\sign\\test2.pdf";
@Test
public void testX() throws Exception {
// 初始化
SignatureInfo signInfo = initSignatureInfo();
PdfboxSignManager pdfboxSign = new PdfboxSignManager(signInfo);
ByteArrayOutputStream outputStream = pdfboxSign.signPDF(signInfo);
File outputFile = new File("D:\\tmp\\sign\\zmgSignTest.pdf");
FileOutputStream fos = new FileOutputStream(outputFile);
outputStream.writeTo(fos);
fos.close();
}
/**
* 初始化签章信息
* @return
*/
private SignatureInfo initSignatureInfo() throws FileNotFoundException {
SignatureInfo signInfo = new SignatureInfo();
signInfo.setCertificateInputStream(new FileInputStream(new File("D:\\tmp\\sign\\signature.p12")));
signInfo.setPassword("123456");
signInfo.setImageInputStream(new FileInputStream(new File("D:\\tmp\\sign\\signature.png")));
signInfo.setSignPdfInputStream(new FileInputStream(new File(PDF_FILE_PATH)));
// 签章附加信息
signInfo.setSignatureAppearance(createSigAppearance());
signInfo.setPageNo(1);
signInfo.setRectllX(400);
signInfo.setRectllY(50);
signInfo.setImageWidth(100);
signInfo.setImageHight(100);
signInfo.setSignatureAlgorithm(SignatureAlgorithm.SHA1);
return signInfo;
}
/**
* 签章附加信息
* @return
*/
private SignatureAppearance createSigAppearance() {
SignatureAppearance sigApp = new SignatureAppearance();
sigApp.setContact("Andy123456");
sigApp.setLocation("Beijing");
sigApp.setReason("I am so cool!!!!!!");
sigApp.setVisibleSignature(true);
return sigApp;
}
}
效果
等下班后再贴图吧