概述
印章是我国特有的历史文化产物,古代主要用作身份凭证和行驶职权的工具。它的起源是由于社会生活的实际需要。早在商周时代,印章就已经产生。如今的印章已成为一种独特的,融实用性和艺术性为一体的艺术瑰宝。传统的印章容易被坏人、小人私刻;从而新闻鲜有报道某某私刻公章,侵吞国家财产。随着计算机技术、加密技术及图像处理技术的发展,出现了电子签章。电子签章是电子签名的一种表现形式,利用图像处理技术、数字加密技术将电子签名操作转化为与纸质文件盖章操作相同的可视效果,同时利用电子签名技术保障电子信息的真实性和完整性以及签名人的不可否认性。
电子签章与数字证书一样是身份验证的一种手段,泛指所有以电子形式存在,依附在电子文件并与其逻辑关联,可用以辨识电子文件签署者身份,保证文件的完整性,并表示签署者同意电子文件所陈述事实的内容。一般来说对电子签章的认定都是从技术角度而言的。主要是指通过特定的技术方案来鉴别当事人的身份及确保电子资料内容不被篡改的安全保障措施。电子签章常于发送安全电子邮件、访问安全站点、网上招标投标、网上签约、安全网上公文传送、公司合同、电子处方笺等。
技术选型
目前主流处理PDF文件两个jar包分别是:
- 开源组织Apache的PDFBox,官网https://pdfbox.apache.org/
- 大名鼎鼎adobe公司的iText,官网https://itextpdf.com/tags/adobe,其中iText又分为iText5和iText7
如何在PDFBox、iText5和iText7选出合适自己项目的技术呢?
对比PDFBox、iText5和iText7这三者:
- PDFBox的功能相对较弱,iText5和iText7的功能非常强悍;
- iText5的资料网上相对较多,如果出现问题容易找到解决方案;PDFBox和iText7的网上资料相对较少,如果出现问题不易找到相关解决方案;
- 通过阅读PDFBox代码目前PDFBox还没提供自定义签章的相关接口;iText5和iText7提供了处理自定义签章的相关实现
- PDFBox只能实现把签章图片加签到PDF文件;iText5和iText7除了可以把签章图片加签到PDF文件,还可以实现直接对签章进行绘制,把文件绘制到签章上。
- 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文件进行电子签章需要实现的功能:
- 生成证书。与PDFBox、iText5和iText7技术无关
- 按模板输出PDF文件:PDFBox、iText5和iText7都可以完成,但是PDFBox会遇到中文乱码比较棘手的问题
- 在PDF文件中实现把签章图片加签到PDF文件:PDFBox、iText5和iText7都可以实现,没有很多的区别
- 在PDF文件中绘制签章:iText5和iText7都可以实现,PDFBox目前不支持
- 在PDF文件中生成高清签章:iText5和iText7都可以实现,PDFBox目前不支持
- 在PDF文件中进行多次签名::PDFBox、iText5和iText7都可以完成,没有区别
通过相关技术分析和要实现的功能分析,采用iText5进行开发,唯一遗憾的是iText商用收费;但是这不是做技术需要关心的!!选用iText5的理由:
- 使用iText5能实现全部的功能
- 如何在开发中遇到相关问题,容易找到相应解决方案
准备相关文件:
1.背景色为空的印章图片
2.扩展名为.p12的证书(参考资料:https://blog.csdn.net/devil_bye/article/details/827591402)
3.freemarker模版
引入相关maven依赖
<!--Freemarker wls-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf -->
<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>
<!-- https://mvnrepository.com/artifact/com.itextpdf.tool/xmlworker -->
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.16</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.56</version>
</dependency>
编写相关代码
1.工具类,第一个函数功能是将freemarker模转换成pdf,第二个函数是给pdf添加电子签章。
package ect.inv.util;
import com.hand.hap.core.IRequest;
import com.hand.hap.fnd.dto.Company;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.security.*;
import com.itextpdf.tool.xml.XMLWorkerHelper;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Map;
/**
* @ClassName: ItextPDFUtil
* @Description: itext导出pdf通用类
* @create: 2019-01-22 11:14
* @Verison:1,0
**/
public class ItextPDFUtil {
private static Logger logger = LoggerFactory.getLogger(ItextPDFUtil.class);
private static String CHINA_TEX_INTER="CHINA_TEX_INTER"; //中纺棉国际
private static String ME_SPIN_COTTON="ME_SPIN_COTTON"; //中纺棉花
/**
* 生成pdf
* @param request
* @param root
* @param pdfName
* @param pngName
* @param docurx
* @param docury
* @return
* @throws TemplateException
* @throws IOException
* @throws Exception
*/
public static ByteArrayOutputStream processPdf(HttpServletRequest request, Map root, String pdfName, String pngName
, Float docurx, Float docury) throws TemplateException, IOException, Exception {
String basePath = request.getSession().getServletContext().getRealPath("/");
Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
//设置加载模板的目录
String ftlUrl = basePath + "/WEB-INF/view/ftl";
logger.info("pdf模板路径:" + ftlUrl);
cfg.setDirectoryForTemplateLoading(new File(ftlUrl));
// 设置编码
cfg.setDefaultEncoding("UTF-8");
logger.info("从指定的模板目录中加载对应的模板文件");
// 从指定的模板目录中加载对应的模板文件
Template temp = cfg.getTemplate("" + pdfName + ".ftl");
root.put("basePath", basePath);
String fileName = basePath + "/WEB-INF/view/" + pdfName + System.currentTimeMillis() + ".html";
logger.info("生成HTML文件名:" + fileName);
File file = new File(fileName);
if (!file.exists()) {
file.createNewFile();
}
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
temp.process(root, out);
String outputFileName = basePath + "/resources/template" + System.currentTimeMillis() + ".pdf";
String outputSignFileName = basePath + "/resources/template" + System.currentTimeMillis() + "sign.pdf";
logger.info("生成PDF文件名:" + outputFileName);
Document document = null;
if (docurx == null || docury == null) {
//默认设置
document = new Document(PageSize.A4); // 横向打印
} else {
document = new Document(new RectangleReadOnly(docurx, docury));
}
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(outputFileName));
document.open();
XMLWorkerHelper.getInstance().parseXHtml(writer, document, new FileInputStream(fileName), Charset.forName("UTF-8"));
document.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String DEST = outputFileName;
if (!StringUtils.isEmpty(pngName)) {
/*=============================电子签章 Start===========================================*/
String KEYSTORE = basePath + "/lib/pdf/zrong2.p12";
char[] PASSWORD = "chinatex".toCharArray();//keystory密码
String SRC = outputFileName;//原始pdf
DEST = outputSignFileName;//签名完成的pdf
String chapterPath = basePath + "/lib/pdf/" + pngName + ".png";//签章图片
String reason = "理由";
String location = "位置";
sign(new FileInputStream(SRC), new FileOutputStream(DEST),
new FileInputStream(KEYSTORE), PASSWORD,
reason, location, chapterPath);
/*=============================电子签章 Start==========================================*/
}
InputStream is = new FileInputStream(DEST);
int buf;
while ((buf = is.read()) != -1) {
baos.write(buf);
}
baos.flush();
is.close();
out.close();
writer.close();
file = new File(fileName);
file.delete();
file = new File(outputFileName);
file.delete();
file = new File(DEST);
file.delete();
return baos;
}
/**
* 在已经生成的pdf上添加电子签章,生成新的pdf并将其输出出来
* @param src
* @param dest
* @param p12Stream
* @param password
* @param reason
* @param location
* @param chapterPath
* @throws GeneralSecurityException
* @throws IOException
* @throws DocumentException
*/
public static void sign(InputStream src //需要签章的pdf文件路径
, OutputStream dest // 签完章的pdf文件路径
, InputStream p12Stream, //p12 路径
char[] password
, String reason //签名的原因,显示在pdf签名属性中,随便填
, String location, String chapterPath) //签名的地点,显示在pdf签名属性中,随便填
throws GeneralSecurityException, IOException, DocumentException {
//读取keystore ,获得私钥和证书链
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(p12Stream, password);
String alias = (String) ks.aliases().nextElement();
PrivateKey pk = (PrivateKey) ks.getKey(alias, password);
Certificate[] chain = ks.getCertificateChain(alias);
//下边的步骤都是固定的,照着写就行了,没啥要解释的
// Creating the reader and the stamper,开始pdfreader
PdfReader reader = new PdfReader(src);
//目标文件输出流
//创建签章工具PdfStamper ,最后一个boolean参数
//false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
//true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
PdfStamper stamper = PdfStamper.createSignature(reader, dest, '\0', null, false);
// 获取数字签章属性对象,设定数字签章的属性
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setReason(reason);
appearance.setLocation(location);
//设置签名的位置,页码,签名域名称,多次追加签名的时候,签名预名称不能一样
//签名的位置,是图章相对于pdf页面的位置坐标,原点为pdf页面左下角
//四个参数的分别是,图章左下角x,图章左下角y,图章右上角x,图章右上角y
appearance.setVisibleSignature(new Rectangle(300, 600, 630, 500), 1, "sig1");
//读取图章图片,这个image是itext包的image
Image image = Image.getInstance(chapterPath);
appearance.setSignatureGraphic(image);
appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
//设置图章的显示方式,如下选择的是只显示图章(还有其他的模式,可以图章和签名描述一同显示)
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
// 这里的itext提供了2个用于签名的接口,可以自己实现,后边着重说这个实现
// 摘要算法
ExternalDigest digest = new BouncyCastleDigest();
// 签名算法
ExternalSignature signature = new PrivateKeySignature(pk, DigestAlgorithms.SHA256, null);
// 调用itext签名方法完成pdf签章CryptoStandard.CMS 签名方式,建议采用这种
MakeSignature.signDetached(appearance, digest, signature, chain, null, null, null, 0, MakeSignature.CryptoStandard.CMS);
}
2.freemarker模版
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Title</title>
<style mce_bogus="1" type="text/css">
.template {
font-family: "SimSun";
color: black;
padding: 10px 40px 10px 40px;
}
.template div {
line-height: 1.5;
}
.header1 {
font-size: 20px;
font-weight: 800;
text-align: center
}
.header2 {
font-family: "SimSun";
font-size: 15px;
font-weight: 800;
text-align: center
}
.table1 table{
line-height: 1;
margin-top: 5px;
width: 100%;
border : 0.5px solid black;
border: 0.5px solid black;
table-layout:fixed;
border-collapse: collapse;
overflow:hidden;
}
.table1 td{
text-align:center;
border: 0.5px solid black;
border: 0.5px solid black;
word-break:break-all;
border-collapse: collapse;
font-size: 15px;
}
</style>
</head>
<body>
<div id="templateNumFiv" class="template">
<p class="header1">${(modelNumTre.companyFullName)?default("")}</p>
<p class="header2">国产棉提货(出库)单</p><br/><br/><br/>
<table width="100%">
<tr>
<td width="70%" style="text-align: left;font-size: 15px;">${(modelNumTre.subinvName)?default("")}:</td>
<td width="30%" style="text-align: left;font-size: 15px;">编号:${(modelNumTre.outNum)?default("")}</td>
</tr>
</table>
<div style="font-size: 15px;"> 请将我司存放在贵仓库的${modelNumTre.wareOutbatches?size}批棉花(重量共计${wareOutWeight?default("")?string("#.######")}吨)的货权转移至${(modelNumTre.exeCustName)?default("")} 名下,请贵仓库给予${(modelNumTre.exeCustName)?default("")}办理提货手续,具体批次如下:</div><br/>
<table class="table1" style="width: 100%;table-layout:fixed;word-break:break-all;padding-bottom: 0px;margin-bottom: 0px;border-bottom: 0px;border-collapse:collapse;" >
<tr>
<td style="width: 10%;text-align: center;" >买方</td>
<td style="width: 45%;text-align: center;" >${(modelNumTre.exeCustName)?default("")}</td>
<td style="width: 15%;text-align: center;">合同号</td>
<td style="width: 30%;text-align: center;" >${(modelNumTre.conNum)?default("")}</td>
</tr>
</table>
<table class="table1" style="width: 100%;table-layout:fixed;word-break:break-all;padding-bottom: 0px;margin-bottom: 0px;border-bottom: 0px;border-collapse:collapse;" >
<tr>
<td style="width: 10%;text-align: center;">序号</td>
<td style="width: 10%;text-align: center;">产地</td>
<td style="width: 25%;text-align: center;">批次</td>
<td style="width: 10%;text-align: center;">件数</td>
<td style="width: 15%;text-align: center;">重量</td>
<td style="width: 20%;text-align: center;">重量标准</td>
<td style="width: 10%;text-align: center;">货位</td>
</tr>
<#if modelNumTre.wareOutbatches?? && (modelNumTre.wareOutbatches?size > 0) >
<#list modelNumTre.wareOutbatches as aim>
<tr>
<td style="width: 10%;text-align: center;">${aim_index+1}</td>
<td style="width: 10%;text-align: center;">${(aim.origin)?default("")}</td>
<td style="width: 25%;text-align: center;">${(aim.batchNum)?default("")}</td>
<td style="width: 10%;text-align: center;">${(aim.batchQty)?default("")}</td>
<td style="width: 15%;text-align: center;">${aim.batchWeight?default("")?string("#.######")}</td>
<td style="width: 20%;text-align: center;">${(modelNumTre.outQualityStand)?default("")}</td>
<td style="width: 10%;text-align: center;">${(aim.loctNum)?default("")}</td>
</tr>
</#list>
<tr>
<td style="width: 10%;text-align: center;">汇总</td>
<td style="width: 10%;text-align: center;"></td>
<td style="width: 25%;text-align: center;"></td>
<td style="width: 10%;text-align: center;">${(wareOutConut)?default("")}</td>
<td style="width: 15%;text-align: center;">${wareOutWeight?default("")?string("#.######")}</td>
<td style="width: 20%;text-align: center;"></td>
<td style="width: 10%;text-align: center;"></td>
</tr>
</#if>
</table>
<br/>
<div style="font-size: 15px">费用承担:<label style="display: inline;" id="fivPay">${(fee)?default("")}</label><br/>
本提货(出库)单传真件、扫描件与原件具有同等法律效力。
</div>
<br/>
<table width="100%" style="">
<tr>
<td style="width:60%;font-size: 15px;"></td>
<td style="width:40%;text-align: center;font-size: 15px;">${(modelNumTre.companyFullName)?default("")}</td>
</tr>
<tr>
<td style="width:60%;font-size: 15px;"></td>
<td style="width:40%;text-align: center;font-size: 15px;">${(orderDate005)?default("")}</td>
</tr>
</table><br/><br/><br/><br/><br/><br/>
<table width="100%">
<tr>
<td style="width:33%;text-align: left;font-size: 15px;">部门经理:</td>
<td style="width:33%;text-align: left;font-size: 15px;">经办人:${(modelNumTre.peopleName)?default("")}</td>
<td style="width:33%;text-align: left;font-size: 15px;"><#if '${isWf?default("")}'=='Y'>审核人:${employeeName?default("")}<#else >审核人:${(actName)?default("")}</#if></td>
</tr>
</table>
<br/>
<table width="100%">
<tr>
<td style="width:100%;text-align: left;font-size: 15px;">收款信息:${(modelNumTre.remark)?default("")}</td>
</tr>
</table>
</div>
</body>
</html>
3.调用电子签章功具类和freemarker模版生成具有电子签章的pdf
@RequestMapping(value = "/ect/inv/ware/outbound/ftlToOnePDF")
@ResponseBody
public void ftlToOnePDF(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,String cottonType,String outId)
throws Exception{
String pdfName="template_inv_ware_outbound_GB_Sign";
String basePath = this.getViewPath();
logger.info("项目路径:"+basePath);
IRequest iRequest = createRequestContext(httpServletRequest);
WareOutbound wareOutbound=new WareOutbound();
wareOutbound.setOutId(Long.parseLong(outId));
wareOutbound = service.self().selectByPrimaryKey(iRequest, wareOutbound);
Company company=new Company();
company.setCompanyId(wareOutbound.getComId());
company=companyMapper.selectByPrimaryKey(company);
ModelAndView data=service.getModelAndViewNumThree(iRequest,wareOutbound,new ModelAndView(),null);
String pngName= ItextPDFUtil.companyToSign(iRequest,company);
ByteArrayOutputStream out= ItextPDFUtil.processPdf(httpServletRequest,data.getModelMap(),pdfName,pngName,595.0F,842.0F);//调用了PDF打印工具类
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/pdf");
OutputStream sOut = httpServletResponse.getOutputStream();
sOut.flush();
sOut.write(out.toByteArray());
sOut.close();
}
4.展示效果

至此,电子签章的代码整理完毕。