java電子簽章實現


項目源碼路徑:https://github.com/Syske/learning-dome-code.git

前言

最近應客戶需求,需要實現電子簽章功能,公章部分用的時金格科技的接口,個人人簽字需要自己實現,公章部分我們就不說了,商業接口做的都比較成熟,也有示例代碼,所以今天着重說的就是個人簽字部分。

參照公章部分的實現方式,同時也參考了很多博客1,查了很多資料,也搞清楚了電子簽章的基本流程:

因為我要實現的功能很簡單就是個人簽章,而且我的簽名是手寫的,所以創建簽名部分就省略了,核心部分就是確定簽名坐標和簽名,確定坐標部分我根據自己查找的資料,實現了根據關鍵字確定坐標,因為確定坐標很麻煩,也不夠靈活。

對於創建簽名我有一個思路,可以將創建簽名作為一個遠程服務部署,然后遠程調用,然后檢驗,返回簽名;當然你也可以通過這種方式生成公章,但是因為沒有經過公正機構認證,這種方式生成的公章其實是不具法律效應的,好了下面直接上代碼吧。

創建簽名密鑰

這個密鑰在pdf簽名的時候會校驗

package io.github.syske.common.util;

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.*;
import java.math.BigInteger;
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 java.security.cert.Certificate;
/**
 * @program: dpf-sign-test
 * @description: 簽名工具類
 * @author: syske
 * @create: 2019-12-04 09:02
 */

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=github,O=github,C=CN,L=西安,ST=陝西";
        String subjectStr = "CN=電子簽名,OU=github,O=github,C=CN,L=西安,ST=陝西";
        String certificateCRL  = "https://syske.github.io/";
        Map<String, byte[]> result = createCert("123456", issuerStr, subjectStr, certificateCRL);

        FileOutputStream outPutStream = new FileOutputStream("D:/keystore.p12"); // ca.jks
        outPutStream.write(result.get("keyStoreData"));
        outPutStream.close();
        FileOutputStream fos = new FileOutputStream(new File("D:/keystore.cer"));
        fos.write(result.get("certificateData"));
        fos.flush();
        fos.close();
    }
}
package io.github.syske.common.util;

/**
 * @program: dpf-sign-test
 * @description:
 * @author: syske
 * @create: 2019-12-04 09:00
 */
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;
    }
}

生成簽名

生成的簽名是png圖片

package io.github.syske.common.util;

/**
 * @program: dpf-sign-test
 * @description:
 * @author: syske
 * @create: 2019-12-03 22:50
 */
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");
    }
}

簽名實現

這里需要注意的是,簽名入參中的頁碼,如果是最后一頁,頁碼傳null

package io.github.syske.common.util;

import com.itextpdf.awt.geom.Rectangle2D;
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.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.itextpdf.text.pdf.security.*;
import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;

import org.apache.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.UUID;

/**
 * @program: SignPdf
 * @description:
 * @author: syske
 * @create: 2019-12-03 22:54
 */

public class SignPdf {
    private static Logger logger = Logger.getLogger(SignPdf.class);
    private static final String PASSWORD = "123456"; // 秘鑰密碼
    private static final String KEY_STORE_PATH = "d:\\keystore.p12"; // 秘鑰文件路徑

    private SignPdf() {
    }

    /**
     * 圖片簽章,指定簽名坐標位置
     *
     * @param signPdfSrc
     *            簽名的PDF文件
     * @param signedPdfOutFile
     *            簽名后的的PDF文件
     * @param signImage
     *            簽名圖片完整路徑
     * @param x
     *            以左下角為原點x坐標值
     * @param y
     *            以左下角為原點Y坐標值
     * @param numberOfPages
     *            簽名頁碼,如果是最后一頁則傳null
     * @param pageStyle
     *            頁面布局,橫向或者縱向
     * @throws Exception
     */
    public static void sign(String signPdfSrc, String signedPdfOutFile,
                            String signImage, Float x, Float y, Integer numberOfPages,
                            PageStyle pageStyle) throws Exception {
        sign(signPdfSrc, signedPdfOutFile, signImage, x, y, null,
                numberOfPages, pageStyle);
    }

    /**
     * 圖片簽章,指定關鍵字
     *
     * @param signPdfSrc
     *            簽名的PDF文件
     * @param signedPdfFile
     *            簽名后的的PDF文件
     * @param signImage
     *            簽名圖片完整路徑
     * @param keyWords
     *            關鍵字
     * @param numberOfPages
     *            簽名頁碼,如果是最后一頁則傳null
     * @param pageStyle
     *            頁面布局,橫向或者縱向
     */
    public static void sign(String signPdfSrc, String signedPdfFile,
                            String signImage, String keyWords, Integer numberOfPages,
                            PageStyle pageStyle) throws Exception {
        sign(signPdfSrc, signedPdfFile, signImage, null, null, keyWords,
                numberOfPages, pageStyle);
    }

    /**
     * 私人簽章
     *
     * @param signPdfSrc
     *            簽名的PDF文件
     * @param signedPdfOutFile
     *            簽名后的的PDF文件
     * @param signImage
     *            簽名圖片完整路徑
     * @param x
     *            以左下角為原點x坐標
     * @param y
     *            以左下角為原點y坐標
     * @param keyWords
     *            關鍵字
     * @param numberOfPages
     *            簽名頁碼,如果是最后一頁則傳null
     * @param pageStyle
     *            頁面布局,橫向或者縱向
     * @return
     */
    public static void sign(String signPdfSrc, String signedPdfOutFile,
                            String signImage, Float x, Float y, String keyWords,
                            Integer numberOfPages, PageStyle pageStyle) throws Exception {
        File signPdfSrcFile = new File(signPdfSrc);
        PdfReader reader = null;
        ByteArrayOutputStream signPDFData = null;
        PdfStamper stp = null;
        FileInputStream fos = null;
        FileOutputStream pdfOutputStream = null;
        try {
            BouncyCastleProvider provider = new BouncyCastleProvider();
            Security.addProvider(provider);
            KeyStore ks = KeyStore.getInstance("PKCS12",
                    new BouncyCastleProvider());
            fos = new FileInputStream(KEY_STORE_PATH);
            // 私鑰密碼 為Pkcs生成證書是的私鑰密碼 123456
            ks.load(fos, PASSWORD.toCharArray());
            String alias = ks.aliases().nextElement();
            PrivateKey key = (PrivateKey) ks.getKey(alias,
                    PASSWORD.toCharArray());
            Certificate[] chain = ks.getCertificateChain(alias);
            reader = new PdfReader(signPdfSrc); // 也可以輸入流的方式構建
            signPDFData = new ByteArrayOutputStream();
            numberOfPages = numberOfPages == null ? reader.getNumberOfPages()
                    : 0;

            // 臨時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);
            float llx = 0f;
            float lly = 0f;

            float signImageWidth = image.getWidth();
            float signImageHeight = image.getHeight();

            float signImageHeightSocale = 85 / signImageWidth * signImageHeight;
            if (keyWords != null && !keyWords.isEmpty()) {
                KeyWordInfo keyWordInfo = getKeyWordLocation(numberOfPages,
                        keyWords, reader);
                Rectangle pageSize = reader.getPageSize(numberOfPages);
                float width = pageSize.getWidth();
                if (PageStyle.PAGE_STYLE_LANDSCAPE.equals(pageStyle)) {
                    llx = keyWordInfo.getY() + (float) keyWordInfo.getHeight();
                    lly = width - keyWordInfo.getX() - signImageHeightSocale
                            / 2;
                } else if (PageStyle.PAGE_STYLE_PORTRAIT.equals(pageStyle)) {
                    llx = keyWordInfo.getX() + (float) keyWordInfo.getWidth()*keyWords.length();
                    lly = keyWordInfo.getY() - signImageHeightSocale / 2;
                }

            } else if (x != null && y != null) {
                llx = x;
                lly = y;
            } else {
                throw new Exception("坐標和關鍵字不能同時為空!");
            }

            float urx = llx + 85;
            float ury = lly + signImageHeightSocale;
            // 是對應x軸和y軸坐標
            sap.setVisibleSignature(new Rectangle(llx, lly, urx, ury),
                    numberOfPages,
                    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();
            pdfOutputStream = new FileOutputStream(signedPdfOutFile);
            pdfOutputStream.write(signPDFData.toByteArray());
            pdfOutputStream.close();
        } catch (KeyStoreException e) {
            logger.error("簽名驗證失敗", e);
            throw new Exception("簽名驗證失敗", e);
        } catch (FileNotFoundException e) {
            logger.error("文件未找到", e);
            throw new Exception("文件未找到", e);
        } catch (IOException e) {
            logger.error("IO異常", e);
            throw new Exception("IO異常", e);
        } catch (Exception e) {
            logger.error("簽章失敗", e);
            throw new Exception("簽章失敗", e);
        } finally {
            if (signPDFData != null) {
                try {
                    signPDFData.close();
                } catch (IOException e) {
                    logger.error("資源關閉失敗", e);
                    throw new Exception("資源關閉失敗", e);
                }
            }

            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    logger.error("資源關閉失敗", e);
                    throw new Exception("資源關閉失敗", e);
                }
            }
            if (pdfOutputStream != null) {
                try {
                    pdfOutputStream.close();
                } catch (IOException e) {
                    logger.error("資源關閉失敗", e);
                    throw new Exception("資源關閉失敗", e);
                }
            }

        }
    }

    /**
     * 查找關鍵字定位
     *
     * @param numberOfPages
     * @param keyWords
     *            關鍵字
     * @param reader
     * @return
     * @throws IOException
     */
    private static KeyWordInfo getKeyWordLocation(Integer numberOfPages,
                                                  final String keyWords, PdfReader reader) throws IOException {
        PdfReaderContentParser pdfReaderContentParser = new PdfReaderContentParser(
                reader);

        final KeyWordInfo keyWordInfo = new KeyWordInfo();

        pdfReaderContentParser.processContent(numberOfPages,
                new RenderListener() {
                    @Override
                    public void renderText(TextRenderInfo textRenderInfo) {
                        String text = textRenderInfo.getText(); // 整頁內容

                        if (null != text && text.contains(keyWords)) {
                            Rectangle2D.Float boundingRectange = textRenderInfo
                                    .getBaseline().getBoundingRectange();
                            float leftY = (float) boundingRectange.getMinY() - 1;
                            float rightY = (float) boundingRectange.getMaxY() + 1;

                            logger.debug(boundingRectange.x + "--"
                                    + boundingRectange.y + "---");

                            keyWordInfo.setHeight(rightY - leftY);
                            keyWordInfo.setWidth((rightY - leftY)
                                    * keyWords.length());
                            keyWordInfo.setX(boundingRectange.x);
                            keyWordInfo.setY(boundingRectange.y);
                        }

                    }

                    @Override
                    public void renderImage(ImageRenderInfo arg0) {}

                    @Override
                    public void endTextBlock() {}

                    @Override
                    public void beginTextBlock() {}
                });
        return keyWordInfo;
    }

    private static class KeyWordInfo {
        private float x;
        private float y;
        private double width;
        private double height;

        public float getX() {
            return x;
        }

        public void setX(float x) {
            this.x = x;
        }

        public float getY() {
            return y;
        }

        public void setY(float y) {
            this.y = y;
        }

        public double getWidth() {
            return width;
        }

        public void setWidth(double width) {
            this.width = width;
        }

        public double getHeight() {
            return height;
        }

        public void setHeight(double height) {
            this.height = height;
        }
    }

    enum PageStyle {
        PAGE_STYLE_LANDSCAPE, // 橫向
        PAGE_STYLE_PORTRAIT // 縱向
    }

    public static PageStyle getPageStyle_LANDSCAPE() {
        return PageStyle.PAGE_STYLE_LANDSCAPE;
    }

    public static PageStyle getPageStyle_PORTRAIT() {
        return PageStyle.PAGE_STYLE_PORTRAIT;
    }

    public static void main(String[] args) throws Exception {
        sign("E:\\test2_sgin.pdf",//
                "D:\\signed-35.pdf",
                "E:\\workSpeace\\pansky\\other_files\\電子簽章\\png\\sign_0000_1_yxz.png",
                null, null, "負責人", null, PageStyle.PAGE_STYLE_LANDSCAPE);
        // read();

        // test();
    }



}

項目pom依賴

差點都忘記了😂

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.github.syske</groupId>
    <artifactId>dpf-sign-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.itextpdf/itext-asian -->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId>
            <version>1.64</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>


    </dependencies>


</project>

簽名預覽:

總結

因為本身業務不復雜,所以也沒有過多的說明,但以上代碼實現部分還有很多需要改進的地方,比如:

1、查找關鍵字的時候,只獲取了一次坐標,但是同一頁關鍵字可能存在多個,如果想在相同關鍵字的地方都蓋章,是不能實現的,如果你正好有這樣的需求,你可以動手自己改造;

2、和第一個類似,沒有實現全文檔簽名

項目源碼路徑:https://github.com/Syske/learning-dome-code.git


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM