PdfBox品尝(III) 签章

何为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;
    }
}
效果

等下班后再贴图吧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342