利用Java對PDF文件進行電子簽章的實戰過程

一、 概述

印章是我國特有的歷史文化產物,古代主要用作身份憑證和行駛職權的工具。它的起源是由於社會生活的實際需要。早在商周時代,印章就已經產生。如今的印章已成為一種獨特的,融實用性和藝術性為一體的藝術瑰寶。傳統的印章容易被壞人、小人私刻;從而新聞鮮有報道某某私刻公章,侵吞國傢財產。隨著計算機技術、加密技術及圖像處理技術的發展,出現瞭電子簽章。電子簽章是電子簽名的一種表現形式,利用圖像處理技術、數字加密技術將電子簽名操作轉化為與紙質文件蓋章操作相同的可視效果,同時利用電子簽名技術保障電子信息的真實性和完整性以及簽名人的不可否認性。

電子簽章與數字證書一樣是身份驗證的一種手段,泛指所有以電子形式存在,依附在電子文件並與其邏輯關聯,可用以辨識電子文件簽署者身份,保證文件的完整性,並表示簽署者同意電子文件所陳述事實的內容。一般來說對電子簽章的認定都是從技術角度而言的。主要是指通過特定的技術方案來鑒別當事人的身份及確保電子資料內容不被篡改的安全保障措施。電子簽章常於發送安全電子郵件、訪問安全站點、網上招標投標、網上簽約、安全網上公文傳送、公司合同、電子處方箋等。

電子簽章是一個很復雜的問題,大到有相關的電子簽章系統;今天分享一下如何把電子簽章應用到電子處方箋的PDF文件裡。

二、 技術選型

目前主流處理PDF文件兩個jar包分別是:

  • 開源組織Apache的PDFBox,官網https://pdfbox.apache.org/
  • 大名鼎鼎adobe公司的iText,官網https://itextpdf.com/tags/adobe,其中iText又分為iText5和iText7

如何在PDFBox、iText5和iText7選出合適自己項目的技術呢?

對比PDFBox、iText5和iText7這三者:

  1. PDFBox的功能相對較弱,iText5和iText7的功能非常強悍;
  2. iText5的資料網上相對較多,如果出現問題容易找到解決方案;PDFBox和iText7的網上資料相對較少,如果出現問題不易找到相關解決方案;
  3. 通過閱讀PDFBox代碼目前PDFBox還沒提供自定義簽章的相關接口;iText5和iText7提供瞭處理自定義簽章的相關實現;
  4. PDFBox隻能實現把簽章圖片加簽到PDF文件;iText5和iText7除瞭可以把簽章圖片加簽到PDF文件,還可以實現直接對簽章進行繪制,把文件繪制到簽章上。
  5. PDFBox和iText5/iText7使用的協議不一樣。PDFBox使用的是APACHE LICENSE VERSION 2.0(https://www.apache.org/licenses/);iText5/iText7使用的是AGPL(https://itextpdf.com/agpl)。PDFBox免費使用,AGPL商用收費

本分享JAVA對PDF文件進行電子簽章需要實現的功能:

  1. 生成證書。與PDFBox、iText5和iText7技術無關
  2. 按模板輸出PDF文件:PDFBox、iText5和iText7都可以完成,但是PDFBox會遇到中文亂碼比較棘手的問題
  3. 在PDF文件中實現把簽章圖片加簽到PDF文件:PDFBox、iText5和iText7都可以實現,沒有很多的區別
  4. 在PDF文件中繪制簽章:iText5和iText7都可以實現,PDFBox目前不支持
  5. 在PDF文件中生成高清簽章:iText5和iText7都可以實現,PDFBox目前不支持
  6. 在PDF文件中進行多次簽名::PDFBox、iText5和iText7都可以完成,沒有區別

通過相關技術分析和要實現的功能分析,采用iText5進行開發,唯一遺憾的是iText商用收費;但是這不是做技術需要關心的!!選用iText5的理由:

  • 使用iText5能實現全部的功能
  • 如何在開發中遇到相關問題,容易找到相應解決方案

三、 生成一個圖片簽章

1. 生成一個如下圖的簽章圖片

2. 相關代碼

    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");
    }
}

四、 如何按模板生成PDF文件

1. 制作PDF模板

目前PDF模板工具別無他物,隻能使用偉大的Adobe公司提供的Adobe Acrobatpro DC軟件進行制作。如何使用該軟件這裡就不多說瞭,如果在使用中遇到什麼可以另外咨詢。

2. 制作一個如下圖的PDF模板,該模板是帶有PDF的表單域的

五、 如何生成PKCS12證書

1. PKCS的簡單介紹

PKCS:The Public-Key Cryptography Standards (簡稱PKCS)是由美國RSA數據安全公司及其合作夥伴制定的一組公鑰密碼學標準,其中包括證書申請、證書更新、證書作廢表發佈、擴展證書內容以及數字簽名、數字信封的格式等方面的一系列相關協議。

到1999年底,PKCS已經公佈瞭以下標準:

  • PKCS#1:定義RSA公開密鑰算法加密和簽名機制,主要用於組織PKCS#7中所描述的數字簽名和數字信封[22]。
  • PKCS#3:定義Diffie-Hellman密鑰交換協議[23]。
  • PKCS#5:描述一種利用從口令派生出來的安全密鑰加密字符串的方法。使用MD2或MD5 從口令中派生密鑰,並采用DES-CBC模式加密。主要用於加密從一個計算機傳送到另一個計算機的私人密鑰,不能用於加密消息[24]。
  • PKCS#6:描述瞭公鑰證書的標準語法,主要描述X.509證書的擴展格式[25]。
  • PKCS#7:定義一種通用的消息語法,包括數字簽名和加密等用於增強的加密機制,PKCS#7與PEM兼容,所以不需其他密碼操作,就可以將加密的消息轉換成PEM消息[26]。
  • PKCS#8:描述私有密鑰信息格式,該信息包括公開密鑰算法的私有密鑰以及可選的屬性集等[27]。
  • PKCS#9:定義一些用於PKCS#6證書擴展、PKCS#7數字簽名和PKCS#8私鑰加密信息的屬性類型[28]。
  • PKCS#10:描述證書請求語法[29]。
  • PKCS#11:稱為Cyptoki,定義瞭一套獨立於技術的程序設計接口,用於智能卡和PCMCIA卡之類的加密設備[30]。
  • PKCS#12:描述個人信息交換語法標準。描述瞭將用戶公鑰、私鑰、證書和其他相關信息打包的語法[31]。
  • PKCS#13:橢圓曲線密碼體制標準[32]。
  • PKCS#14:偽隨機數生成標準。
  • PKCS#15:密碼令牌信息格式標準[33]。

PKCS12也就是以上標準的PKCS#12,主要用來描述個人身份信息;本次分享中要進行簽章操作的是醫生和藥師,他們就是一個個人主體,給他們分配一個PKCS12的證書,就等於給他們分配瞭一個用於蓋章的印章。

2. 使用JAVA生成一個PKCS12證書並進行存貯,相關分析見代碼註解

    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;
        }
    }
 
 
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.math.BigInteger;
    import java.security.KeyPair;
    import java.security.KeyPairGenerator;
    import java.security.KeyStore;
    import java.security.NoSuchAlgorithmException;
    import java.security.PrivateKey;
    import java.security.PublicKey;
    import java.security.SecureRandom;
    import java.security.cert.Certificate;
    import java.security.cert.CertificateFactory;
    import java.security.cert.X509Certificate;
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Random;
 
    import org.bouncycastle.asn1.ASN1ObjectIdentifier;
    import org.bouncycastle.asn1.ASN1Primitive;
    import org.bouncycastle.asn1.x500.X500Name;
    import org.bouncycastle.asn1.x509.BasicConstraints;
    import org.bouncycastle.asn1.x509.CRLDistPoint;
    import org.bouncycastle.asn1.x509.DistributionPoint;
    import org.bouncycastle.asn1.x509.DistributionPointName;
    import org.bouncycastle.asn1.x509.GeneralName;
    import org.bouncycastle.asn1.x509.GeneralNames;
    import org.bouncycastle.asn1.x509.KeyUsage;
    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;
 
    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=gitbook研發部,O=gitbook有限公司,C=CN,[email protected],L=北京,ST=北京";
        String subjectStr = "CN=huangjinjin,OU=gitbook研發部,O=gitbook有限公司,C=CN,[email protected],L=北京,ST=北京";
        String certificateCRL  = "https://gitbook.cn";
        Map<String, byte[]> result = createCert("123456", issuerStr, subjectStr, certificateCRL);
 
        FileOutputStream outPutStream = new FileOutputStream("c:/keystore.p12"); // ca.jks
        outPutStream.write(result.get("keyStoreData"));
        outPutStream.close();
        FileOutputStream fos = new FileOutputStream(new File("c:/keystore.cer"));
        fos.write(result.get("certificateData"));
        fos.flush();
        fos.close();
    }
    }

六、 如何生成一個高清晰的簽章

1. 由PDF模板生成一個PDF文件,見代碼註解

    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import com.itextpdf.text.DocumentException;
    import com.itextpdf.text.pdf.AcroFields;
    import com.itextpdf.text.pdf.AcroFields.Item;
    import com.itextpdf.text.pdf.BaseFont;
    import com.itextpdf.text.pdf.PdfReader;
    import com.itextpdf.text.pdf.PdfStamper;
 
    public class PDFUtils {
 
 
    /**
     * @param fields
     * @param data
     * @throws IOException
     * @throws DocumentException
     */
    private static void fillData(AcroFields fields, Map<String, String> data) throws IOException, DocumentException {
        List<String> keys = new ArrayList<String>();
        Map<String, Item> formFields = fields.getFields();
        for (String key : data.keySet()) {
            if(formFields.containsKey(key)){
                String value = data.get(key);
                fields.setField(key, value); // 為字段賦值,註意字段名稱是區分大小寫的
                keys.add(key);
            }
        }
        Iterator<String> itemsKey = formFields.keySet().iterator();
        while(itemsKey.hasNext()){
            String itemKey = itemsKey.next();
            if(!keys.contains(itemKey)){
                fields.setField(itemKey, " ");
            }
        }
    }
 
    /**
     * @param templatePdfPath
     *            模板pdf路徑
     * @param generatePdfPath
     *            生成pdf路徑
     * @param data
     *            數據
     */
    public static String generatePDF(String templatePdfPath, String generatePdfPath, Map<String, String> data) {
        OutputStream fos = null;
        ByteArrayOutputStream bos = null;
        try {
            PdfReader reader = new PdfReader(templatePdfPath);
            bos = new ByteArrayOutputStream();
            /* 將要生成的目標PDF文件名稱 */
            PdfStamper ps = new PdfStamper(reader, bos);
            /* 使用中文字體 */
            BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);
            ArrayList<BaseFont> fontList = new ArrayList<BaseFont>();
            fontList.add(bf);
            /* 取出報表模板中的所有字段 */
            AcroFields fields = ps.getAcroFields();
            fields.setSubstitutionFonts(fontList);
            fillData(fields, data);
            /* 必須要調用這個,否則文檔不會生成的  如果為false那麼生成的PDF文件還能編輯,一定要設為true*/
            ps.setFormFlattening(true);
            ps.close();
            fos = new FileOutputStream(generatePdfPath);
            fos.write(bos.toByteArray());
            fos.flush();
            return generatePdfPath;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
 
    public static void main(String[] args) {
        Map<String, String> data = new HashMap<String, String>();
        //key為pdf模板的form表單的名字,value為需要填充的值
        data.put("title", "在線醫院");
        data.put("case", "123456789");
        data.put("date", "2018.12.07");
        data.put("name", "gitbook");
        data.put("sex", "男");
        data.put("age", "29");
        data.put("phone", "13711645814");
        data.put("office", "內科");
        data.put("cert", "身癢找打");
        data.put("drug", "1、奧美拉唑腸溶膠囊             0.25g10粒×2板 ");
        data.put("dose", "×2盒");
        data.put("cons", "用法用量:口服 一日兩次 一次2粒");
        data.put("tips", "溫馨提示");
        data.put("desc", "盡量呆在通風較好的地方,保持空氣流通,有利於病情康復。盡量呆在通風較好的地方");
        generatePDF("C:\\Users\\zhilin\\Desktop\\chat\\tpl.pdf", 
                "C:\\Users\\zhilin\\Desktop\\chat\\filled.pdf", data );
    }
    }

2. 對PDF文件進行簽章

經過過上面的代碼可以生成一個名為sign.jpg的簽章圖片,生成一個keystore.p12的證書文件,還有一個已經通過模板填充瞭表單的名為filled.pdf的pdf文件。下面就可通過以上材料生成一個簽名的PDF文件。

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.security.KeyStore;
    import java.security.PrivateKey;
    import java.security.Security;
    import java.security.cert.Certificate;
    import java.util.UUID;
 
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
 
    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.security.BouncyCastleDigest;
    import com.itextpdf.text.pdf.security.DigestAlgorithms;
    import com.itextpdf.text.pdf.security.ExternalDigest;
    import com.itextpdf.text.pdf.security.ExternalSignature;
    import com.itextpdf.text.pdf.security.MakeSignature;
    import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
    import com.itextpdf.text.pdf.security.PrivateKeySignature;
 
 
    public class SignPdf {
    /**
     * @param password
     *            秘鑰密碼
     * @param keyStorePath
     *            秘鑰文件路徑
     * @param signPdfSrc
     *            簽名的PDF文件
     * @param signImage
     *            簽名圖片文件
     * @param x
     *            x坐標
     * @param y
     *            y坐標
     * @return
     */
    public static byte[] sign(String password, String keyStorePath, String signPdfSrc, String signImage,
            float x, float y) {
        File signPdfSrcFile = new File(signPdfSrc);
        PdfReader reader = null;
        ByteArrayOutputStream signPDFData = null;
        PdfStamper stp = null;
        FileInputStream fos = null;
        try {
            BouncyCastleProvider provider = new BouncyCastleProvider();
            Security.addProvider(provider);
            KeyStore ks = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
            fos = new FileInputStream(keyStorePath);
            // 私鑰密碼 為Pkcs生成證書是的私鑰密碼 123456
            ks.load(fos, password.toCharArray()); 
            String alias = (String) ks.aliases().nextElement();
            PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray());
            Certificate[] chain = ks.getCertificateChain(alias);
            reader = new PdfReader(signPdfSrc);
            signPDFData = new ByteArrayOutputStream();
            // 臨時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);
            // 是對應x軸和y軸坐標
            sap.setVisibleSignature(new Rectangle(x, y, x + 185, y + 68), 1,
                    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();
            return signPDFData.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
 
            if (signPDFData != null) {
                try {
                    signPDFData.close();
                } catch (IOException e) {
                }
            }
 
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                }
            }
        }
        return null;
    }
 
    public static void main(String[] args) throws Exception {
        byte[] fileData = sign("123456", "C:\\Users\\zhilin\\Desktop\\chat\\keystore.p12", //
                "C:\\Users\\zhilin\\Desktop\\chat\\filled.pdf",//
                "C:\\Users\\zhilin\\Desktop\\chat\\sign.jpg", 100, 290);
        FileOutputStream f = new FileOutputStream(new File("C:\\Users\\zhilin\\Desktop\\chat\\signed.pdf"));
        f.write(fileData);
        f.close();
    }
    }

3. 高清簽章

高清簽章是通過iText的繪制功能來完成。主要直接在PDF文件中繪制簽章,代碼實現如下:

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.security.KeyStore;
    import java.security.PrivateKey;
    import java.security.Security;
    import java.security.cert.Certificate;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
 
    import com.itextpdf.awt.AsianFontMapper;
    import com.itextpdf.text.BaseColor;
    import com.itextpdf.text.Element;
    import com.itextpdf.text.Font;
    import com.itextpdf.text.Paragraph;
    import com.itextpdf.text.Rectangle;
    import com.itextpdf.text.pdf.BaseFont;
    import com.itextpdf.text.pdf.ColumnText;
    import com.itextpdf.text.pdf.PdfReader;
    import com.itextpdf.text.pdf.PdfSignatureAppearance;
    import com.itextpdf.text.pdf.PdfStamper;
    import com.itextpdf.text.pdf.PdfStream;
    import com.itextpdf.text.pdf.PdfTemplate;
    import com.itextpdf.text.pdf.security.BouncyCastleDigest;
    import com.itextpdf.text.pdf.security.DigestAlgorithms;
    import com.itextpdf.text.pdf.security.ExternalDigest;
    import com.itextpdf.text.pdf.security.ExternalSignature;
    import com.itextpdf.text.pdf.security.MakeSignature;
    import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
    import com.itextpdf.text.pdf.security.PrivateKeySignature;
 
 
    public class SignHighPdf {
 
    /**
     * @param password
     *            秘鑰密碼
     * @param keyStorePath
     *            秘鑰文件路徑
     * @param signPdfSrc
     *            簽名的PDF文件
     * @param x
     * 
     * @param y
     * @return
     */
    public static byte[] sign(String password, String keyStorePath, String signPdfSrc, 
            float x, float y,
            String signText) {
        File signPdfSrcFile = new File(signPdfSrc);
        PdfReader reader = null;
        ByteArrayOutputStream signPDFData = null;
        PdfStamper stp = null;
        FileInputStream fos = null;
        try {
            BouncyCastleProvider provider = new BouncyCastleProvider();
            Security.addProvider(provider);
            KeyStore ks = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
            fos = new FileInputStream(keyStorePath);
            ks.load(fos, password.toCharArray()); // 私鑰密碼
            String alias = (String) ks.aliases().nextElement();
            PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray());
            Certificate[] chain = ks.getCertificateChain(alias);
            reader = new PdfReader(signPdfSrc);
            signPDFData = new ByteArrayOutputStream();
            // 臨時pdf文件
            File temp = new File(signPdfSrcFile.getParent(), System.currentTimeMillis() + ".pdf");
            stp = PdfStamper.createSignature(reader, signPDFData, '\0', temp, true);
            PdfSignatureAppearance sap = stp.getSignatureAppearance();
            sap.setReason("數字簽名,不可改變");
            // 是對應x軸和y軸坐標
            sap.setVisibleSignature(new Rectangle(x, y, x + 150, y + 65), 1,
                    "sr"+String.valueOf(System.nanoTime()));
            /layer 0 Creating the appearance for layer 0
            PdfTemplate n0 = sap.getLayer(0);
            n0.reset();
            float lx = n0.getBoundingBox().getLeft();
            float by = n0.getBoundingBox().getBottom();
            float width = n0.getBoundingBox().getWidth();
            float height = n0.getBoundingBox().getHeight();
            n0.setRGBColorFill(255, 0, 0);
            n0.rectangle(lx, by, 5, height);
            n0.rectangle(lx, by, width, 5);
            n0.rectangle(lx, by+height-5, width, 5);
            n0.rectangle(lx+width-5, by, 5, height);
            n0.fill();
            ///layer 2
            PdfTemplate n2 = sap.getLayer(2);
            n2.setCharacterSpacing(0.0f);
            ColumnText ct = new ColumnText(n2);
            ct.setSimpleColumn(n2.getBoundingBox());
            n2.setRGBColorFill(255, 0, 0);
            //做一個占位的動作
            Paragraph p1 = new Paragraph(" ");
            BaseFont bf = BaseFont.createFont(AsianFontMapper.ChineseSimplifiedFont, AsianFontMapper.ChineseSimplifiedEncoding_H, 
                    BaseFont.NOT_EMBEDDED);
            Font font1 = new Font(bf, 5, Font.BOLD, BaseColor.RED);
            Font font2 = new Font(bf, 13, Font.BOLD, BaseColor.RED);
            p1.setFont(font1);
            ct.addElement(p1);
            Paragraph p = new Paragraph(signText);
            p.setAlignment(Element.ALIGN_CENTER);
            p.setFont(font2);
            ct.addElement(p);
            ct.go();
            stp.getWriter().setCompressionLevel(PdfStream.BEST_COMPRESSION);
            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();
            return signPDFData.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (signPDFData != null) {
                try {
                    signPDFData.close();
                } catch (IOException e) {
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                }
            }
        }
        return null;
    }
 
    public static void main(String[] args) throws Exception {
        //對已經簽章的signed.pdf文件再次簽章,這次是高清簽章
        byte[] fileData = sign("123456", "C:\\Users\\zhilin\\Desktop\\chat\\keystore.p12",//
                "C:\\Users\\zhilin\\Desktop\\chat\\signed.pdf", 350, 290, "華佗\n2017-12-20");
        FileOutputStream f = new FileOutputStream(new File("C:\\Users\\zhilin\\Desktop\\chat\\signed2.pdf"));
        f.write(fileData);
        f.close();
    }
 
    }

可以分析下下面這兩個簽章的區別,發現左邊的簽章很模糊,右邊的特別清晰。

七、 如何進行多次PDF簽名

生成多個簽章重點代碼,已在SignPdf.java類進行標註說明;如果想進行多次簽名,就隻需對已經進行過簽名的PDF文件再次調用sign方法進行再次簽名即可(第六點有張圖片就有兩個簽章,這就是多次簽名的結果)。

PdfStamper.createSignature(reader, signPDFData, '\0', temp, true);

八、 總結

分享中sign.jpg文件的白色背景需要做透明化處理才能達到正確電子簽章的效果(不覆蓋PDF文件中已有的內容,真實的電子簽章也是這樣做的),大傢回去可以思考下怎麼把一個jpg文件白色背景透明化(高清簽章就已經實現透明化,可以試著把SignPdf.java和SignHighPdf.java簽章到有文字的PDF上面看看效果)。

大傢見到的公司公章都是圓形的;這個也是可以做到的大傢想想怎樣生成一個圓形的圖片簽章;然後進行電子簽名。這裡主要是講解代碼實現,所有代碼非常多。大傢回去好好研讀代碼。真正的電子簽名需要通過CA認證公司來完成,我這裡隻是提供參考方案讓大傢學習。

到此這篇關於利用Java對PDF文件進行電子簽章的文章就介紹到這瞭,更多相關Java對PDF文件電子簽章內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: