Java 签章操作的设计与实现

1 说明

      本文主要讲解使用Java和SpringBoot框架设计实现对PDF的签章操作。实现对PDF的签章操作不是简单的找个图片贴到PDF上即可,而是需要申请数字证书才能对PDF签章,否则无法验证签章的身份。具体实现分两步进行,第一步生成PKCS12证书,第二步添加签章。

2 生成PKCS12证书

      生成PKCS12证书是通过使用bouncycastle包实现的,具体实现如下所示,首先引入依赖。

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

步骤1: 编写生成PKCS12证书工具类

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
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 com.css.modules.file.entity.Extension;

/**
 * 证书生成工具类
 *
 * @author 一朝风月
 * @date 2021/8/6 14:20
 */
public class GenerateCertificateUtil {
    private static KeyPair getKey() throws NoSuchAlgorithmException {
        // 密钥对 生成器,RSA算法 生成的  提供者是 BouncyCastle
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
        // 密钥长度 1024
        generator.initialize(1024);
        // 证书中的密钥 公钥和私钥
        return generator.generateKeyPair();
    }

    /**
     * 创建证书
     *
     * @param password       密码
     * @param issuerStr      颁发机构信息
     * @param subjectStr     使用者信息
     * @param certificateCrl 颁发地址
     * @return 证书
     */
    public static Map<String, byte[]> createCert(String password, String issuerStr, String subjectStr,
                                                 String certificateCrl, List<Extension> extensions) {

        Map<String, byte[]> result = new HashMap<>();
        ByteArrayOutputStream out = null;
        try {
            // 生成证书
            // KeyStore keyStore = KeyStore.getInstance("JKS");
            KeyStore keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
            keyStore.load(null, null);
            KeyPair keyPair = getKey();
            //  issuer与 subject相同的证书就是CA证书
            Certificate cert = generateCertificate(issuerStr, subjectStr, keyPair, result, certificateCrl, extensions);
            // 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 {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 生成证书
     *
     * @param issuerStr      事项字符串
     * @param subjectStr     主题字符串
     * @param keyPair        密钥对(公钥,私钥)
     * @param result         结果
     * @param certificateCrl CRL分发点
     * @param extensions     扩展字段
     * @return 生成证书
     */
    private static Certificate generateCertificate(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 identifier = 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(identifier, 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));
            // privateKey:使用自己的私钥进行签名,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(StandardCharsets.UTF_8));
            //证书有效结束时间
            result.put("notAfter", format.format(notAfter).getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bout != null) {
                try {
                    bout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return cert;
    }
}

步骤2: 证书参数类

import lombok.Data;

/**
 * 证书参数
 *
 * @author 一朝风月
 * @date 2021/8/6 16:06
 */
@Data
public class CertificateParams {
    /**
     * 事项字符串
     */
    private String issuerStr;
    /**
     * 主题字符串
     */
    private String subjectStr;
    /**
     * CRL分发点
     */
    private String certificateCrl;
    /**
     * 证书密码
     */
    private String password;
    /**
     * 额外的信息
     */
    private String extensionsJson;
}

步骤3: 编写获取证书接口

	/**
     * 获取PKCS12证书
     *
     * @param params   证书参数
     * @param response HTTP响应
     * @return PKCS12证书
     * @throws IOException 异常
     */
    @PostMapping("getCertificate")
    public Result mGetCertificate(CertificateParams params, HttpServletResponse response) throws IOException {
        /*
         * 填写说明
         *
         * CN: 名字与姓氏 OU : 组织单位名称
         * O :组织名称 L : 城市或区域名称 E : 电子邮件
         * ST: 州或省份名称 C: 单位的两字母国-家代码
         *
         * 例如:
         * String issuerStr = "CN=jcb凭证,OU=研发部,O=jcb有限公司,C=CN,E=jcb@sina.com,L=北京,ST=北京";
         * String subjectStr = "CN=jcb有限公司,OU=用户,O=test,C=CN,E=jcb@sina.com,L=北京,ST=北京";
         * String certificateCrl = "https://jcb.cn";
         */
        Map<String, byte[]> result = GenerateCertificateUtil.createCert(params.getPassword(), params.getIssuerStr(),
                params.getSubjectStr(), params.getCertificateCrl(),
                JSONArray.parseArray(params.getExtensionsJson(), Extension.class));
        // 将证书放置在response中
        response.setContentType("application/*");
        response.setHeader("Content-Disposition", "attachment;filename=\"" + "PKCS12证书" + "\"");
        IOUtils.write(result.get("keyStoreData"), response.getOutputStream());
        return null;
    }

3 使用证书添加签章

      使用刚刚生成的证书添加签章,依赖如下所示:

		<dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13</version>
        </dependency>

3.1 签章参数类

import lombok.Data;

/**
 * 签章参数
 *
 * @author 一朝风月
 * @date 2021/8/6 10:12
 */
@Data
public class SignSealParams {
    /**
     * 原因
     */
    private String reason;
    /**
     * 位置
     */
    private String location;
    /**
     * 页码
     */
    private int pageNum;
    /**
     * 签名域名称,多次追加签名的时候,签名域名称不能一样
     */
    private String fieldName;
    /**
     * 证书密码
     */
    private String password;
    /**
     * 图章左下角x
     */
    private int llx;
    /**
     * 图章左下角y
     */
    private int lly;
    /**
     * 图章右上角x
     */
    private int urx;
    /**
     * 图章右上角y
     */
    private int ury;

    SignSealParams() {
        this.pageNum = 1;
        this.fieldName = "sig1";
        this.llx = 200;
        this.lly = 200;
        this.urx = 300;
        this.ury = 300;
    }
}

3.2 添加签章

	/**
     * 添加签章
     *
     * @param file        PDF
     * @param seal        印章或者签名
     * @param certificate PKCS12证书
     * @param params      参数
     * @param response    HTTP响应
     * @return 返回增加签章的PDF
     * @throws Exception 异常
     */
    @PostMapping("addSeal")
    public Result mAddSeal(MultipartFile file, MultipartFile seal, MultipartFile certificate,
                           SignSealParams params, HttpServletResponse response) throws Exception {
        FileProps fileProps = new FileProps(file);
        if (!"pdf".equalsIgnoreCase(fileProps.getFileType())) {
            return Result.error("请传入PDF文件!");
        }

        PdfReader reader = new PdfReader(file.getInputStream());

        response.setContentType("application/*");
        response.setHeader("Content-Disposition", "attachment;filename=\"" + fileProps.getFileName() + "\"");
        /*
         * 创建签章工具PdfStamper 最后一个boolean参数,
         * false:pdf文件只允许被签名一次,多次签名,最后一次有效
         * true:pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
         */
        PdfStamper stamper = PdfStamper.createSignature(reader, response.getOutputStream(),
                '\0', null, true);
        // 获取数字签章属性对象,设定数字签章的属性
        PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
        appearance.setReason(params.getReason());
        appearance.setLocation(params.getLocation());
        appearance.setVisibleSignature(
                new Rectangle(params.getLlx(), params.getLly(), params.getUrx(), params.getUry()),
                params.getPageNum(), params.getFieldName());

        Image image = Image.getInstance(seal.getBytes());
        appearance.setSignatureGraphic(image);
        appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
        appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
        ExternalDigest digest = new BouncyCastleDigest();
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(certificate.getInputStream(), params.getPassword().toCharArray());
        String alias = ks.aliases().nextElement();
        PrivateKey pk = (PrivateKey) ks.getKey(alias, params.getPassword().toCharArray());
        Certificate[] chain = ks.getCertificateChain(alias);

        ExternalSignature signature = new PrivateKeySignature(pk, DigestAlgorithms.SHA1, null);
        MakeSignature.signDetached(appearance, digest, signature, chain, null,
                null, null, 0, CryptoStandard.CMS);
        return null;
    }

4 结果

      测试一下上述代码的执行结果

在这里插入图片描述

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值