如何用 Java 对 PDF 文件进行电子签章

2019-04-01 22:48:25


一、 概述

印章是我国特有的历史文化产物,古代主要用作身份凭证和行驶职权的工具。它的起源是由于社会生活的实际需要。早在商周时代,印章就已经产生。如今的印章已成为一种独特的,融实用性和艺术性为一体的艺术瑰宝。传统的印章容易被坏人、小人私刻;从而新闻鲜有报道某某私刻公章,侵吞国家财产。随着计算机技术、加密技术及图像处理技术的发展,出现了电子签章。电子签章是电子签名的一种表现形式,利用图像处理技术、数字加密技术将电子签名操作转化为与纸质文件盖章操作相同的可视效果,同时利用电子签名技术保障电子信息的真实性和完整性以及签名人的不可否认性。

电子签章与数字证书一样是身份验证的一种手段,泛指所有以电子形式存在,依附在电子文件并与其逻辑关联,可用以辨识电子文件签署者身份,保证文件的完整性,并表示签署者同意电子文件所陈述事实的内容。一般来说对电子签章的认定都是从技术角度而言的。主要是指通过特定的技术方案来鉴别当事人的身份及确保电子资料内容不被篡改的安全保障措施。电子签章常于发送安全电子邮件、访问安全站点、网上招标投标、网上签约、安全网上公文传送、公司合同、电子处方笺等。

电子签章是一个很复杂的问题,大到有相关的电子签章系统;今天分享一下如何把电子签章应用到电子处方笺的PDF文件里。

二、 技术选型

目前主流处理PDF文件两个jar包分别是:

  1. 开源组织Apache的PDFBox,官网https://pdfbox.apache.org/

  2. 大名鼎鼎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的理由:

  1. 使用iText5能实现全部的功能

  2. 如何在开发中遇到相关问题,容易找到相应解决方案

三、 生成一个图片签章

1. 生成一个如下图的签章图片

enter image description here

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的表单域的

enter image description here

五、 如何生成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.keyCertSignbuilder.addExtension(keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));// 基本限制 X509Extension.javaASN1ObjectIdentifier 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,E=gitbook@sina.com,L=北京,ST=北京";String subjectStr = "CN=huangjinjin,OU=gitbook研发部,O=gitbook有限公司,C=CN,E=huangjinjin@sina.com,L=北京,ST=北京";String certificateCRL  = "https://gitbook.cn";Map<String, byte[]> result = createCert("123456", issuerStr, subjectStr, certificateCRL);FileOutputStream outPutStream = new FileOutputStream("c:/keystore.p12"); // ca.jksoutPutStream.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 );}}

enter image description here

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生成证书是的私钥密码 123456ks.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();}}

enter image description here

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 0PdfTemplate 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 2PdfTemplate 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();}}

可以分析下下面这两个签章的区别,发现左边的签章很模糊,右边的特别清晰。

enter image description here

七、 如何进行多次PDF签名

生成多个签章重点代码,已在SignPdf.java类进行标注说明;如果想进行多次签名,就只需对已经进行过签名的PDF文件再次调用sign方法进行再次签名即可(第六点有张图片就有两个签章,这就是多次签名的结果)。

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

八、 总结

分享中sign.jpg文件的白色背景需要做透明化处理才能达到正确电子签章的效果(不覆盖PDF文件中已有的内容,真实的电子签章也是这样做的),大家回去可以思考下怎么把一个jpg文件白色背景透明化(高清签章就已经实现透明化,可以试着把SignPdf.java和SignHighPdf.java签章到有文字的PDF上面看看效果)。

大家见到的公司公章都是圆形的;这个也是可以做到的大家想想怎样生成一个圆形的图片签章;然后进行电子签名。这里主要是讲解代码实现,所有代码非常多。大家回去好好研读代码。真正的电子签名需要通过CA认证公司来完成,我这里只是提供参考方案让大家学习。


  • 2019-09-08 15:09:14

    IDEA从mapper.java跳转到mapper.xml

    在IDEA中写项目后台的时候,从controller到service到mapper到dao,都可以直接跳转,但是mapper.java到mapper.xml就需要自行寻找,为了开发方便,安装相应插件--mybais

  • 2019-09-08 21:44:15

    git pre-commit hook failed 解决办法

    今天在上传项目的时候在commit阶段遇到一个问题,无论是在Sourcetree上传还是用命令git commit -m 'xxx'都报了一下错误:

  • 2019-09-08 21:45:31

    git index.lock

    因是在你进行某些比较费时的git操作时自动生成,操作结束后自动删除,相当于一个锁定文件,目的在于防止对一个目录同时进行多个操作。 有时强制关闭进行中的git操作,这个文件没有被自动删除,之后你就无法进行其他操作,必须手动删除,进入.git文件中删除,打开显示隐藏文件。如果没有看见.git文件夹,可以直接用命令rm -f ./.git/index.lock。之后就可以正常使用。 ———————————————— 版权声明:本文为CSDN博主「李瑞豪」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_39520417/article/details/81941111

  • 2019-09-09 05:28:54

    IntelliJ IDEA 进阶教程: 语言注入

    我真的太久没发 IntelliJ 的教程了,最近 Sakura 同学找我帮他配 IntelliJ ,我秀了一下技术,假装自己是老司机。 然后发现 Language Injection 这个非常好用的功能我居然没写教程(很明显,一堆人不知道这个东西), 于是我又开始写教程了。 本文多图。

  • 2019-09-09 05:32:55

    SrpingBoot 热启动与热部署

    当我们已经启动了一个服务,然后修改代码之后,会自动重新部署。Spring项目通常有新内容修改后需要重新编译然后运行。通过配置Spring boot的热启动配置,可以实现自动编译重启项目,通常要比手动停止,启动项目快。