Spring Boot集成iText实现电子签章-二 实战

2.1 生成数字证书

首先我们需要生成一个数字证书。

这个数字证书我们可以利用 JDK 自带的工具生成,为了贴近实战,松哥这里使用 Java 代码生成,生成数字证书的方式如下。

首先引入 Bouncy Castle,Bouncy Castle 是一个广泛使用的开源加密库,它为 Java 平台提供了丰富的密码学算法实现,包括对称加密、非对称加密、哈希算法、数字签名等。这个库由于其广泛的算法支持和可靠性而备受信任,被许多安全应用和加密通信协议所采用

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.70</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-ext-jdk15on</artifactId>
    <version>1.70</version>
</dependency>

接下来我们写一个生成数字证书的工具类,如下:

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
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.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author:江南一点雨
 * @site:http://www.javaboy.org
 * @微信公众号:江南一点雨
 * @github:https://github.com/lenve
 * @gitee:https://gitee.com/lenve
 */
public class PkcsUtils {

    /**
     * 生成证书
     *
     * @return
     * @throws NoSuchAlgorithmException
     */
    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[]>();
        try(ByteArrayOutputStream out= new ByteArrayOutputStream()) {
            // 标志生成PKCS12证书
            KeyStore keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
            keyStore.load(null, null);
            KeyPair keyPair = getKey();
            // issuer与 subject相同的证书就是CA证书
            X509Certificate cert = generateCertificateV3(issuerStr, subjectStr,
                    keyPair, result, certificateCRL);
            // 证书序列号
            keyStore.setKeyEntry("cretkey", keyPair.getPrivate(),
                    password.toCharArray(), new X509Certificate[]{cert});
            cert.verify(keyPair.getPublic());
            keyStore.store(out, password.toCharArray());
            byte[] keyStoreData = out.toByteArray();
            result.put("keyStoreData", keyStoreData);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 生成证书
     * @param issuerStr
     * @param subjectStr
     * @param keyPair
     * @param result
     * @param certificateCRL
     * @return
     */
    public static X509Certificate generateCertificateV3(String issuerStr,
                                                        String subjectStr, KeyPair keyPair, Map<String, byte[]> result,
                                                        String certificateCRL) {
        ByteArrayInputStream bint = 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));
            X509CertificateHolder holder = builder.build(singer);
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            bint = new ByteArrayInputStream(holder.toASN1Structure()
                    .getEncoded());
            cert = (X509Certificate) cf.generateCertificate(bint);
            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 (bint != null) {
                try {
                    bint.close();
                } catch (IOException e) {
                }
            }
        }
        return cert;
    }

    public static void main(String[] args) throws Exception {
        // CN: 名字与姓氏    OU : 组织单位名称
        // O :组织名称  L : 城市或区域名称  E : 电子邮件
        // ST: 州或省份名称  C: 单位的两字母国家代码
        String issuerStr = "CN=javaboy,OU=产品研发部,O=江南一点雨,C=CN,E=javaboy@gmail.com,L=华南,ST=深圳";
        String subjectStr = "CN=javaboy,OU=产品研发部,O=江南一点雨,C=CN,E=javaboy@gmail.com,L=华南,ST=深圳";
        String certificateCRL = "http://www.javaboy.org";
        Map<String, byte[]> result = createCert("123456", issuerStr, subjectStr, certificateCRL);

        FileOutputStream outPutStream = new FileOutputStream("keystore.p12");
        outPutStream.write(result.get("keyStoreData"));
        outPutStream.close();
        FileOutputStream fos = new FileOutputStream(new File("keystore.cer"));
        fos.write(result.get("certificateData"));
        fos.flush();
        fos.close();
    }

}

运行这个工具代码,会在我们当前工程目录下生成 keystore.p12keystore.cer 两个文件。

其中 keystore.cer 文件通常是一个以 DER 或 PEM 格式存储的 X.509 公钥证书,它包含了公钥以及证书所有者的信息,如姓名、组织、地理位置等。

keystore.p12 文件是一个 PKCS#12 格式的文件,它是一个个人信息交换标准,用于存储一个或多个证书以及它们对应的私钥。.p12 文件是加密的,通常需要密码才能打开。这种文件格式便于将证书和私钥一起分发或存储,常用于需要在不同系统或设备间传输证书和私钥的场景。

总结下就是,.cer 文件通常只包含公钥证书,而 .p12 文件可以包含证书和私钥。

2.2 生成印章图片

接下来我们用 Java 代码绘制一个签章图片,如下:

public class SealSample {
    public static void main(String[] args) throws Exception {
        Seal seal = new Seal();
        seal.setSize(200);
        SealCircle sealCircle = new SealCircle();
        sealCircle.setLine(4);
        sealCircle.setWidth(95);
        sealCircle.setHeight(95);
        seal.setBorderCircle(sealCircle);
        SealFont mainFont = new SealFont();
        mainFont.setText("江南一点雨股份有限公司");
        mainFont.setSize(22);
        mainFont.setFamily("隶书");
        mainFont.setSpace(22.0);
        mainFont.setMargin(4);
        seal.setMainFont(mainFont);
        SealFont centerFont = new SealFont();
        centerFont.setText("★");
        centerFont.setSize(60);
        seal.setCenterFont(centerFont);
        SealFont titleFont = new SealFont();
        titleFont.setText("财务专用章");
        titleFont.setSize(16);
        titleFont.setSpace(8.0);
        titleFont.setMargin(54);
        seal.setTitleFont(titleFont);
        seal.draw("公章1.png");
    }
}

这里涉及到的一些工具类文末可以下载。

最终生成的签章图片类似下面这样:

现在万事具备,可以给 PDF 签名了。

2.3 PDF 签名

最后,我们可以通过如下代码为 PDF 进行签名。

这里我们通过 iText 来实现电子签章,因此需要先引入 iText:

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.13.4</version>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>html2pdf</artifactId>
    <version>5.0.5</version>
</dependency>

接下来对 PDF 文件进行签名:

public class SignPdf2 {

    /**
     * @param password pkcs12证书密码
     * @param keyStorePath pkcs12证书路径
     * @param signPdfSrc 签名pdf路径
     * @param signImage 签名图片
     * @param x
     * @param 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(PdfSignatureAppearance.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, MakeSignature.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", "keystore.p12", 
                "待签名.pdf",//
                "公章1.png", 100, 290);
        FileOutputStream f = new FileOutputStream(new File("已签名.pdf"));
        f.write(fileData);
        f.close();
    }
}

这里所需要的参数基本上前文都提过了,不再多说。

从表面上看,签名结束之后,PDF 文件上多了一个印章,如下:

本质上,则是该 PDF 文件多了一个签名信息,通过 Adobe 的 PDF 软件可以查看,如下:

之所以显示签名有效性未知,是因为我们使用的是自己生成的数字证书,如果从权威机构申请的数字证书,就不会出现这个提示。

好啦,是不是很 easy?

上一篇:Java面试题之HashMap


下一篇:ELK同时采集Nginx、linux内核日志信息