├── .gitignore ├── LICENSE ├── README.md ├── docs └── images │ ├── pdf-cert.png │ ├── product.png │ ├── seal-template.png │ ├── seal-ym.png │ ├── signature.png │ └── verify.png ├── pom.xml └── src └── main ├── java └── com │ └── resrun │ ├── OpenSign.java │ ├── controller │ ├── OpenSignController.java │ ├── TestController.java │ └── vo │ │ ├── base │ │ └── Result.java │ │ ├── request │ │ ├── ClipSealRequest.java │ │ ├── GenerateSealRequest.java │ │ ├── PositionRequest.java │ │ ├── SignRequest.java │ │ └── VerifyRequest.java │ │ └── response │ │ ├── SealResponse.java │ │ ├── SignResponse.java │ │ └── VerifyResponse.java │ ├── enums │ ├── SealColorEnum.java │ ├── SignStatus.java │ └── SignTypeEnum.java │ ├── service │ ├── cert │ │ └── CertService.java │ ├── image │ │ ├── DateSealService.java │ │ ├── EntSealClipService.java │ │ ├── EntSealGenerateService.java │ │ ├── PdfConvertImageService.java │ │ └── PersonalSealService.java │ ├── pdf │ │ ├── CalculatePositionService.java │ │ └── SignService.java │ ├── pojo │ │ ├── BaseCertificateInfo.java │ │ ├── CertificateProperty.java │ │ ├── ConvertImage.java │ │ ├── GenerateCertificateInfo.java │ │ ├── RealPositionProperty.java │ │ ├── SelectKeywords.java │ │ ├── SignPdfInfoVo.java │ │ └── SourcePositionProperty.java │ └── verify │ │ ├── ExtImageRenderListener.java │ │ ├── SignVerifyService.java │ │ └── VerifySign.java │ └── utils │ └── Base64.java └── resources ├── application-dev.yml ├── application.yml └── 开源工具版说明.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/** 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | 25 | ### NetBeans ### 26 | /nbproject/private/ 27 | /nbbuild/ 28 | /dist/ 29 | /nbdist/ 30 | /.nb-gradle/ 31 | build/ 32 | !**/src/main/**/build/ 33 | !**/src/test/**/build/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 kaifangqian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

开放签电子签章工具版源码

2 | 3 | #### 项目简介 4 | 将电子签章的核心技术代码和工具进行开源,开源版采用更加宽松的MIT开源协议,且不受商业限制。产品功能包括:电子印章制作,手写签名生成,数字证书生成,PDF文件转图片,电子签章(关键字签署、指定位置签署),文件验签等 5 | 6 | #### 项目源码 7 | | | 后端源码 | 前端源码 | 8 | |--- |--- | --- | 9 | | github | https://github.com/kaifangqian/open-sign | https://github.com/kaifangqian/open-sign-vue | 10 | | 码云 | https://gitee.com/kaifangqian/open-sign | https://gitee.com/kaifangqian/open-sign-vue | 11 | 12 | #### 官网地址 13 | [https://www.kaifangqian.com](https://www.kaifangqian.com) 14 | 15 | #### 体验地址 16 | [https://demo.kaifangqian.com](https://demo.kaifangqian.com) 17 | 18 | #### 工具库说明 19 | ##### 手写签名面板 20 | * 提供个人手写签名面板的前端页面,生成手写签名图片。 21 | ![手写签名面板](./docs/images/signature.png) 22 | 23 | 24 | ##### 电子印章制作 25 | * 为企业生成电子印章,生成印章的方式有两种:
26 | * 系统生成:根据印章环绕文字、横排文字生成电子印章图片; 27 | ![系统生成](./docs/images/seal-template.png) 28 | * 印模生成: 29 | * 在白纸上加盖印章; 30 | * 拍照或扫描上传,对印模图片进行裁剪; 31 | * 对裁剪后的印模图片进行自动透明化抠图,生成透明印章; 32 | ![印模生成](./docs/images/seal-ym.png) 33 | 34 | ##### 数字证书(公钥加密技术生成,非CA数字证书)生成 35 | * 生成用于测试电子签章的数字证书,该数字证书使用公钥加密技术进行生成,主要用于帮助开发者跑通电子签章流程,非CA机构签发,不具备法律效力!
36 | * 以下从功能和用途上对SSL证书和CA证书进行简单介绍:

37 | * SSL证书和CA证书都是数字证书,但它们在功能和用途上存在明显的区别。

38 | * CA证书由权威的数字证书颁发机构(CA)颁发。在电子签章过程中,CA数字证书可以验证签署者的身份,将签名数据与数字证书中的公钥进行加密,以保证签名的安全和可靠性。同时,CA证书还可以确保签署的文件没有被篡改,保证电子信息的完整性、保密性和不可抵赖性。通过使用CA数字证书,可以确认签署主体的身份,确保签名是符合法律规定的、是防篡改的,让电子合同和纸质合同具有同等法律效力。在签署电子合同之前,用户需要完成实名认证,由CA机构来签发数字证书,确认签署主体的身份。因此,CA数字证书在电子签章中起到了关键的作用,可以确保签署过程的合法性和安全性,使得电子签署文件具有更高的可信度和法律效力。

39 | * SSL证书是一种加密协议,用于保护网站和用户之间的数据传输安全。SSL证书通过使用公钥加密技术,将用户的敏感信息(如信用卡号、密码等)加密传输,以防止被恶意第三方截获和窃取。同时,SSL证书还可以验证网站的身份,确保用户与正确的网站进行通信。因此,SSL证书主要用于加密网站和用户之间的数据传输,保护用户的隐私和安全。

40 | 41 | ![数字证书](./docs/images/pdf-cert.png) 42 | 43 | ##### PDF文件转图片 44 | 45 | * 提供PDF文件转图片的工具类。
46 | * 电子签章过程中存在着在网页上对签署文件进行预览、指定签署位置、文件签署等操作,由于图片在浏览器上的兼容性和友好性优于PDF文件,所以一般在网页上进行电子签章时,会先将PDF文件转换成图片,展示给用户。用户在页面上确定好签署位置,并进行签署时,后端服务会通过对电子印章/手写签名位置、大小以及PDF文件的大小进行计算,在PDF文件的准确位置上完成文件签署。 47 | 48 | ##### 电子文件签署 49 | 50 | * 电子文件签署是指对电子文件进行电子签章的过程。
51 | * 开放签-开源版提供了两种电子签章的方式,如下:
52 | * 关键字签署:指定PDF文件中需要进行签章的关键字,签署时,会在文档中查找对应关键字的位置,并加盖电子印章/手写签名,完成电子签章; 53 | * 指定位置签署:直接指定在PDF文件中需要加盖电子印章/手写签名的位置,签署时,直接在指定位置上完成电子签章。 54 | 55 | ![电子文件签署](./docs/images/product.png) 56 | 57 | ##### 签署文件验证 58 | * 验证电子文件是否被篡改以及文件中对应的电子印章/手写签名和数字证书信息。
59 | * 验证一般存在以下几种情况:
60 | * PDF文件中不存在数字签名,即该文件未进行过电子签章,无需验证; 61 | * PDF文件中存在数字证书,进行签名验证时发生错误,签名包含不正确的、无法识别的、已损坏的或可疑的数据,即文件的签名信息被篡改,验证不通过; 62 | * PDF文件中存在数字证书,且数字证书均可识别,但文件被篡改,验证不通过; 63 | * PDF文件中存在数字证书,文档未被篡改,验证通过。 64 | 65 | ![签署文件验证](./docs/images/verify.png) 66 | 67 | 68 | 69 | #### Build Setup 70 | ``` 71 | #进入项目目录 72 | cd open-sign 73 | 74 | #进行打包 75 | mvn clean package 76 | 77 | #运行jar包 78 | java -jar target/opensign-demo.jar 79 | ``` 80 | 81 | 82 | #### 代码结构 83 | ``` 84 | - open-sign 开放签工具 85 | - controller 提供演示接口 86 | - enums 公共枚举类 87 | - service 签约服务 88 | - cert 证书服务 89 | - image 印模图片处理、PDF转图片处理 90 | - pdf 签署位置计算、PDF文件签署 91 | - verify PDF文件验签 92 | - utils 工具类 93 | ``` 94 | 95 | #### 反馈交流 96 | - QQ交流群:482074553 -------------------------------------------------------------------------------- /docs/images/pdf-cert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenghuihan/open-sign/f1fa38ee6e9ce4dc467123c67264c6f88b1d4559/docs/images/pdf-cert.png -------------------------------------------------------------------------------- /docs/images/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenghuihan/open-sign/f1fa38ee6e9ce4dc467123c67264c6f88b1d4559/docs/images/product.png -------------------------------------------------------------------------------- /docs/images/seal-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenghuihan/open-sign/f1fa38ee6e9ce4dc467123c67264c6f88b1d4559/docs/images/seal-template.png -------------------------------------------------------------------------------- /docs/images/seal-ym.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenghuihan/open-sign/f1fa38ee6e9ce4dc467123c67264c6f88b1d4559/docs/images/seal-ym.png -------------------------------------------------------------------------------- /docs/images/signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenghuihan/open-sign/f1fa38ee6e9ce4dc467123c67264c6f88b1d4559/docs/images/signature.png -------------------------------------------------------------------------------- /docs/images/verify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenghuihan/open-sign/f1fa38ee6e9ce4dc467123c67264c6f88b1d4559/docs/images/verify.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.resrun 8 | openSign 9 | 1.0 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.7.18 15 | 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | org.yaml 25 | snakeyaml 26 | 27 | 28 | 29 | 30 | 31 | org.yaml 32 | snakeyaml 33 | 2.2 34 | 35 | 36 | 37 | 38 | 39 | org.apache.pdfbox 40 | pdfbox-tools 41 | 2.0.27 42 | 43 | 44 | com.itextpdf.tool 45 | xmlworker 46 | 5.5.13 47 | 48 | 49 | com.itextpdf 50 | itextpdf 51 | 5.5.13.3 52 | 53 | 54 | 55 | 56 | org.bouncycastle 57 | bcpkix-jdk15on 58 | 1.70 59 | 60 | 61 | org.bouncycastle 62 | bcprov-ext-jdk15on 63 | 1.70 64 | 65 | 66 | 67 | org.projectlombok 68 | lombok 69 | 1.18.24 70 | 71 | 72 | 73 | io.swagger 74 | swagger-models 75 | 1.6.12 76 | 77 | 78 | 79 | 80 | 81 | 8 82 | 8 83 | UTF-8 84 | 85 | 86 | 87 | 88 | public 89 | aliyun nexus 90 | https://repo1.maven.org/maven2/ 91 | 92 | 93 | 94 | 95 | opensign-demo 96 | 97 | 98 | org.springframework.boot 99 | spring-boot-maven-plugin 100 | 101 | 102 | false 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/OpenSign.java: -------------------------------------------------------------------------------- 1 | package com.resrun; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class OpenSign { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(OpenSign.class, args); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/OpenSignController.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller; 2 | 3 | import com.resrun.enums.SignTypeEnum; 4 | import com.resrun.service.pojo.CertificateProperty; 5 | import com.resrun.service.pojo.GenerateCertificateInfo; 6 | import com.resrun.service.pojo.RealPositionProperty; 7 | import com.resrun.service.pojo.SourcePositionProperty; 8 | import com.resrun.service.cert.CertService; 9 | import com.resrun.service.image.EntSealClipService; 10 | import com.resrun.service.image.EntSealGenerateService; 11 | import com.resrun.service.pdf.CalculatePositionService; 12 | import com.resrun.service.pdf.SignService; 13 | import com.resrun.service.verify.SignVerifyService; 14 | import com.resrun.utils.Base64; 15 | import com.resrun.controller.vo.base.Result; 16 | import com.resrun.controller.vo.request.*; 17 | import com.resrun.controller.vo.response.SealResponse; 18 | import com.resrun.controller.vo.response.SignResponse; 19 | import com.resrun.controller.vo.response.VerifyResponse; 20 | import io.swagger.annotations.Api; 21 | import io.swagger.annotations.ApiOperation; 22 | import org.apache.pdfbox.io.IOUtils; 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.beans.factory.annotation.Value; 25 | import org.springframework.util.ResourceUtils; 26 | import org.springframework.web.bind.annotation.*; 27 | 28 | import java.io.*; 29 | import java.util.ArrayList; 30 | import java.util.List; 31 | 32 | /** 33 | * @Description: OpenSignController 34 | * @Package: com.resrun.controller 35 | * @ClassName: OpenSignController 36 | * @copyright 北京资源律动科技有限公司 37 | */ 38 | @Api(tags = "开放签-演示demo") 39 | @RestController 40 | public class OpenSignController { 41 | 42 | 43 | @Autowired 44 | private SignVerifyService signVerifyService ; 45 | @Autowired 46 | private CalculatePositionService calculatePositionService ; 47 | @Autowired 48 | private SignService signService ; 49 | @Autowired 50 | private EntSealGenerateService entSealGenerateService ; 51 | @Autowired 52 | private EntSealClipService entSealClipService ; 53 | @Autowired 54 | private CertService certService ; 55 | 56 | @ApiOperation("生成企业签章-上传生成") 57 | @RequestMapping(value = "/clip/seal",method = RequestMethod.POST) 58 | public Result generateUpload(@RequestBody ClipSealRequest request){ 59 | 60 | 61 | if(request.getImage() == null || request.getImage().length() == 0){ 62 | return Result.error("图片数据为空",null); 63 | } 64 | byte[] decode = Base64.decode(request.getImage()); 65 | if(decode == null || decode.length == 0){ 66 | return Result.error("签章制作失败",null); 67 | } 68 | 69 | byte[] entSealByte = entSealClipService.clip(decode, request.getColorRange()); 70 | if(entSealByte == null){ 71 | return Result.error("签章制作失败",null); 72 | } 73 | String encode = Base64.encode(entSealByte); 74 | SealResponse response = new SealResponse(); 75 | response.setEntSeal(encode); 76 | return Result.OK(response) ; 77 | 78 | } 79 | 80 | @ApiOperation("生成企业签章-参数生成") 81 | @RequestMapping(value = "/generate/seal",method = RequestMethod.POST) 82 | public Result seal(@RequestBody GenerateSealRequest request){ 83 | 84 | 85 | if(request == null || request.getMiddleText() == null || request.getTopText() == null){ 86 | return Result.error("参数缺失",null) ; 87 | } 88 | byte[] entSealByte = entSealGenerateService.generateEntSeal(request.getTopText(), request.getMiddleText()); 89 | if(entSealByte == null){ 90 | return Result.error("签章制作失败",null); 91 | } 92 | String encode = Base64.encode(entSealByte); 93 | SealResponse response = new SealResponse(); 94 | response.setEntSeal(encode); 95 | return Result.OK(response) ; 96 | 97 | } 98 | 99 | @ApiOperation("签署") 100 | @RequestMapping(value = "/sign",method = RequestMethod.POST) 101 | public Result sign(@RequestBody SignRequest request){ 102 | 103 | String fileName = "开源工具版说明.pdf" ; 104 | byte[] signFileBytes = null ; 105 | byte[] entSealBytes = null ; 106 | byte[] personalBytes = null ; 107 | CertificateProperty entCert = null ; 108 | CertificateProperty personalCert = null ; 109 | List entPositionList = null; 110 | List personalPositionList = null; 111 | int entSealWidth = 200 ; 112 | int entSealHeight = 200 ; 113 | int personalSealWidth = 150 ; 114 | int personalSealHeight = 70 ; 115 | //获取本地签署文件 116 | try { 117 | signFileBytes = getResourceFiles(fileName); 118 | } catch (Exception e) { 119 | e.printStackTrace(); 120 | } 121 | if(signFileBytes == null){ 122 | return Result.error("签署失败",null); 123 | } 124 | //生成企业证书和个人证书 125 | try { 126 | if(request.getEntName() != null && request.getEntName().length() > 0){ 127 | String subject = "C=CN,ST=北京,L=北京,O=开放签 CA,OU=产品部,CN=开放签@" + request.getEntName(); 128 | GenerateCertificateInfo generateCertificateInfo = certService.generateCertificate(null, subject, 10); 129 | if(generateCertificateInfo != null){ 130 | entCert = new CertificateProperty(); 131 | entCert.setCertType("PKCS12"); 132 | entCert.setCertFile(generateCertificateInfo.getPfx()); 133 | entCert.setPassword(generateCertificateInfo.getPassword()); 134 | } 135 | if(entCert == null){ 136 | return Result.error("签署失败",null); 137 | } 138 | } 139 | if(request.getPersonalName() != null && request.getPersonalName().length() > 0){ 140 | String subject = "C=CN,ST=北京,L=北京,O=开放签 CA,OU=产品部,CN=开放签@" + request.getPersonalName(); 141 | GenerateCertificateInfo generateCertificateInfo = certService.generateCertificate(null, subject, 10); 142 | if(generateCertificateInfo != null){ 143 | personalCert = new CertificateProperty(); 144 | personalCert.setCertType("PKCS12"); 145 | personalCert.setCertFile(generateCertificateInfo.getPfx()); 146 | personalCert.setPassword(generateCertificateInfo.getPassword()); 147 | } 148 | if(personalCert == null){ 149 | return Result.error("签署失败",null); 150 | } 151 | } 152 | } catch (Exception e) { 153 | e.printStackTrace(); 154 | } 155 | 156 | 157 | //生成企业签章和个人签章 158 | if(request.getEntSeal() != null){ 159 | entSealBytes = Base64.decode(request.getEntSeal()); 160 | } 161 | if(request.getPersonalSeal() != null){ 162 | personalBytes = Base64.decode(request.getPersonalSeal()); 163 | } 164 | 165 | //计算企业签署位置和个人签署位置 166 | if(SignTypeEnum.POSITION.getCode().equals(request.getSignType())){ 167 | if((request.getEntPositionList() == null || request.getEntPositionList().size() == 0 ) && 168 | (request.getPersonalPositionList() == null || request.getPersonalPositionList().size() == 0)){ 169 | return Result.error("签署失败",null); 170 | } 171 | //计算企业签署位置 172 | if(request.getEntPositionList() != null && request.getEntPositionList().size() > 0){ 173 | List convert = convert(request.getEntPositionList()); 174 | entPositionList = calculatePositionService.calculatePositions(convert, signFileBytes); 175 | } 176 | //计算个人签署位置 177 | if(request.getPersonalPositionList() != null && request.getPersonalPositionList().size() > 0){ 178 | List convert = convert(request.getPersonalPositionList()); 179 | personalPositionList = calculatePositionService.calculatePositions(convert, signFileBytes); 180 | } 181 | }else if(SignTypeEnum.KEYWORD.getCode().equals(request.getSignType())){ 182 | if((request.getEntKeyword() == null || request.getEntKeyword().length() == 0 ) && 183 | (request.getPersonalKeyword() == null || request.getPersonalKeyword().length() == 0)){ 184 | return Result.error("签署失败",null); 185 | } 186 | //根据关键字计算所有企业签署位置 187 | if(request.getEntKeyword() != null && request.getEntKeyword().length() > 0){ 188 | entPositionList = calculatePositionService.getAllPositionByKeyWords(signFileBytes, request.getEntKeyword(), entSealWidth, entSealHeight); 189 | } 190 | //根据关键字计算所有个人签署位置 191 | if(request.getPersonalKeyword() != null && request.getPersonalKeyword().length() > 0){ 192 | personalPositionList = calculatePositionService.getAllPositionByKeyWords(signFileBytes,request.getPersonalKeyword(),personalSealWidth,personalSealHeight); 193 | } 194 | if((personalPositionList == null || personalPositionList.size() == 0 ) && 195 | (personalPositionList == null || personalPositionList.size() == 0)){ 196 | return Result.error("签署失败!签署关键字在文件中不存在,请准确设置关键字后再签署",null); 197 | } 198 | } 199 | 200 | //进行签署操作 201 | byte[] operationByte = signFileBytes ; 202 | try { 203 | //所有企业位置签署 204 | if(entPositionList.size() > 0){ 205 | for(RealPositionProperty realPositionProperty : entPositionList){ 206 | operationByte = signService.signingContract(operationByte, entSealBytes, entCert, realPositionProperty); 207 | } 208 | } 209 | //所有个人位置签署 210 | if(personalPositionList.size() > 0){ 211 | for(RealPositionProperty realPositionProperty : personalPositionList){ 212 | operationByte = signService.signingContract(operationByte, personalBytes, personalCert, realPositionProperty); 213 | } 214 | } 215 | }catch (Exception e){ 216 | 217 | } 218 | if(operationByte == null){ 219 | return Result.error("签署失败",null); 220 | } 221 | 222 | // try { 223 | // org.apache.commons.io.IOUtils.write(operationByte,new FileOutputStream(new File("/Users/gongfenglai/Desktop/test/pdf/" + System.currentTimeMillis() + ".pdf"))); 224 | // } catch (Exception e) { 225 | // e.printStackTrace(); 226 | // } 227 | 228 | String encode = Base64.encode(operationByte); 229 | SignResponse response = new SignResponse(); 230 | response.setSignFile(encode); 231 | return Result.OK(response); 232 | 233 | } 234 | 235 | @ApiOperation("文件验签") 236 | @RequestMapping(value = "/verify",method = RequestMethod.POST) 237 | public Result verify(@RequestBody VerifyRequest request){ 238 | 239 | String verifyFile = request.getVerifyFile(); 240 | byte[] decode = Base64.decode(verifyFile); 241 | 242 | VerifyResponse imageFromPdf = signVerifyService.getImageFromPdf(decode, request.getFileName()); 243 | return Result.OK(imageFromPdf) ; 244 | } 245 | 246 | 247 | public List convert(List positionRequestList){ 248 | List list = new ArrayList<>(); 249 | for(PositionRequest request : positionRequestList){ 250 | SourcePositionProperty position = new SourcePositionProperty(); 251 | position.setOffsetX(Float.valueOf(request.getOffsetX())); 252 | position.setOffsetY(Float.valueOf(request.getOffsetY())); 253 | position.setPage(request.getPage()); 254 | position.setWidth(Float.valueOf(request.getWidth())); 255 | position.setHeight(Float.valueOf(request.getHeight())); 256 | position.setPageHeight(Float.valueOf(request.getPageHeight())); 257 | position.setPageWidth(Float.valueOf(request.getPageWidth())); 258 | list.add(position); 259 | } 260 | return list ; 261 | } 262 | 263 | 264 | public byte [] getResourceFiles(String path) { 265 | try { 266 | InputStream inputStream = ResourceUtils.class.getClassLoader() 267 | .getResourceAsStream(path); 268 | return read(inputStream); 269 | }catch (Exception e){ 270 | System.err.println(path); 271 | e.printStackTrace(); 272 | } 273 | return null; 274 | } 275 | 276 | 277 | public byte[] read(InputStream inputStream) throws IOException { 278 | try { 279 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 280 | byte[] buffer = new byte[1024]; 281 | int num = inputStream.read(buffer); 282 | while (num != -1) { 283 | baos.write(buffer, 0, num); 284 | num = inputStream.read(buffer); 285 | } 286 | baos.flush(); 287 | return baos.toByteArray(); 288 | } finally { 289 | if (inputStream != null) { 290 | inputStream.close(); 291 | } 292 | } 293 | } 294 | 295 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | /** 8 | * @author : zhenghuihan 9 | * create at: 2023/12/7 15:09 10 | * @description: 11 | */ 12 | @RestController 13 | @RequestMapping("/api") 14 | public class TestController { 15 | 16 | /** 17 | * @description 测试 18 | */ 19 | @GetMapping(value = "/test") 20 | public String test() { 21 | return "ok"; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/base/Result.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.base; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @Description: Result 13 | * @Package: com.resrun.controller.vo 14 | * @ClassName: Result 15 | * @copyright 北京资源律动科技有限公司 16 | */ 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @ApiModel("统一返回对象") 21 | public class Result implements Serializable { 22 | 23 | private static final long serialVersionUID = 4186430915088458662L; 24 | 25 | @ApiModelProperty("成功标志") 26 | private boolean success = true; 27 | @ApiModelProperty("返回处理消息") 28 | private String message = ""; 29 | @ApiModelProperty("返回代码") 30 | private Integer code = 200; 31 | @ApiModelProperty("返回数据对象") 32 | private T result; 33 | @ApiModelProperty("时间戳") 34 | private long timestamp = System.currentTimeMillis(); 35 | 36 | 37 | public static Result OK(T data) { 38 | Result r = new Result(); 39 | r.setSuccess(true); 40 | r.setCode(200); 41 | r.setResult(data); 42 | return r; 43 | } 44 | 45 | 46 | 47 | public static Result error(String msg, T data) { 48 | Result r = new Result(); 49 | r.setSuccess(false); 50 | r.setCode(500); 51 | r.setMessage(msg); 52 | r.setResult(data); 53 | return r; 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/request/ClipSealRequest.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.request; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @Description: ClipSealRequest 13 | * @Package: com.resrun.controller.vo.request 14 | * @ClassName: ClipSealRequest 15 | * @copyright 北京资源律动科技有限公司 16 | */ 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @ApiModel("企业印章上传生成-请求对象") 21 | public class ClipSealRequest implements Serializable { 22 | 23 | private static final long serialVersionUID = 7532064967450524188L; 24 | 25 | @ApiModelProperty("图片base64") 26 | private String image ; 27 | 28 | @ApiModelProperty("色差范围0~255") 29 | private Integer colorRange ; 30 | 31 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/request/GenerateSealRequest.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.request; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @Description: SealRequest 13 | * @Package: com.resrun.controller.vo.request 14 | * @ClassName: SealRequest 15 | * @copyright 北京资源律动科技有限公司 16 | */ 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @ApiModel("企业印章参数生成-请求对象") 21 | public class GenerateSealRequest implements Serializable { 22 | 23 | private static final long serialVersionUID = -7107056549410243340L; 24 | 25 | @ApiModelProperty("企业名称") 26 | private String topText; 27 | @ApiModelProperty("横排文字") 28 | private String middleText ; 29 | 30 | 31 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/request/PositionRequest.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.request; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @Description: PositionRequest 13 | * @Package: com.resrun.controller.vo.request 14 | * @ClassName: PositionRequest 15 | * @copyright 北京资源律动科技有限公司 16 | */ 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @ApiModel("签署位置-请求对象") 21 | public class PositionRequest implements Serializable { 22 | 23 | private static final long serialVersionUID = 1407897694551031954L; 24 | 25 | @ApiModelProperty("控件X坐标(左上角)") 26 | private String offsetX ; 27 | 28 | @ApiModelProperty("控件Y坐标(左上角)") 29 | private String offsetY ; 30 | 31 | @ApiModelProperty("控件宽度") 32 | private String width ; 33 | 34 | @ApiModelProperty("控件高度") 35 | private String height ; 36 | 37 | @ApiModelProperty("控件所属页码") 38 | private Integer page ; 39 | 40 | @ApiModelProperty("当前文件页面宽度") 41 | private String pageWidth ; 42 | 43 | @ApiModelProperty("当前文件页面高度") 44 | private String pageHeight ; 45 | 46 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/request/SignRequest.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.request; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | import java.util.List; 11 | 12 | /** 13 | * @Description: SignRequest 14 | * @Package: com.resrun.controller.vo.request 15 | * @ClassName: SignRequest 16 | * @copyright 北京资源律动科技有限公司 17 | */ 18 | @Data 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | @ApiModel("签署-请求对象") 22 | public class SignRequest implements Serializable { 23 | 24 | @ApiModelProperty("签署类型,1位置签署,2关键字签署") 25 | private Integer signType ; 26 | 27 | @ApiModelProperty("企业签章base64") 28 | private String entSeal ; 29 | 30 | @ApiModelProperty("企业名称") 31 | private String entName ; 32 | 33 | @ApiModelProperty("个人签章base64") 34 | private String personalSeal ; 35 | 36 | @ApiModelProperty("个人名称") 37 | private String personalName ; 38 | 39 | @ApiModelProperty("企业签署位置") 40 | private List entPositionList ; 41 | 42 | @ApiModelProperty("个人签章位置") 43 | private List personalPositionList ; 44 | 45 | @ApiModelProperty("企业签章关键字") 46 | private String entKeyword ; 47 | 48 | @ApiModelProperty("个人签章关键字") 49 | private String personalKeyword ; 50 | 51 | 52 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/request/VerifyRequest.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.request; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @Description: VerifyRequest 13 | * @Package: com.resrun.controller.vo.request 14 | * @ClassName: VerifyRequest 15 | * @copyright 北京资源律动科技有限公司 16 | */ 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @ApiModel("文件验签-请求对象") 21 | public class VerifyRequest implements Serializable { 22 | 23 | private static final long serialVersionUID = -5412004997017887483L; 24 | 25 | @ApiModelProperty("验签文件base64") 26 | private String verifyFile ; 27 | 28 | @ApiModelProperty("验签文件名称") 29 | private String fileName ; 30 | 31 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/response/SealResponse.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.response; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @Description: SealResponse 13 | * @Package: com.resrun.controller.vo.response 14 | * @ClassName: SealResponse 15 | * @copyright 北京资源律动科技有限公司 16 | */ 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @ApiModel("企业印章生成-返回对象") 21 | public class SealResponse implements Serializable { 22 | 23 | private static final long serialVersionUID = 3376686508340918420L; 24 | 25 | @ApiModelProperty("企业签章base64") 26 | private String entSeal ; 27 | 28 | 29 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/response/SignResponse.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.response; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @Description: SignResponse 13 | * @Package: com.resrun.controller.vo.response 14 | * @ClassName: SignResponse 15 | * @copyright 北京资源律动科技有限公司 16 | */ 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @ApiModel("文件签署-返回对象") 21 | public class SignResponse implements Serializable { 22 | 23 | private static final long serialVersionUID = 7191345743000212815L; 24 | 25 | @ApiModelProperty("签署后文件") 26 | private String signFile ; 27 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/controller/vo/response/VerifyResponse.java: -------------------------------------------------------------------------------- 1 | package com.resrun.controller.vo.response; 2 | 3 | import com.resrun.service.pojo.SignPdfInfoVo; 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.io.Serializable; 11 | 12 | /** 13 | * @Description: VerifyRequest 14 | * @Package: com.resrun.controller.vo.response 15 | * @ClassName: VerifyRequest 16 | * @copyright 北京资源律动科技有限公司 17 | */ 18 | @Data 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | @ApiModel("文件验签-返回对象") 22 | public class VerifyResponse implements Serializable { 23 | 24 | private static final long serialVersionUID = -1498109564641142303L; 25 | 26 | @ApiModelProperty("返回信息") 27 | private String msg ; 28 | 29 | @ApiModelProperty("详细数据") 30 | private SignPdfInfoVo vo ; 31 | 32 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/enums/SealColorEnum.java: -------------------------------------------------------------------------------- 1 | package com.resrun.enums; 2 | 3 | /** 4 | * @Description: 签章颜色枚举类 5 | * @Package: com.resrun.enums 6 | * @ClassName: SealColorEnum 7 | * @copyright 北京资源律动科技有限公司 8 | */ 9 | public enum SealColorEnum { 10 | 11 | RED(1,"红色"), 12 | BLUE(2,"蓝色"), 13 | BLACK(3,"黑色"), 14 | 15 | ; 16 | 17 | private Integer code ; 18 | private String name ; 19 | 20 | SealColorEnum(Integer code, String name){ 21 | this.code = code ; 22 | this.name = name ; 23 | } 24 | 25 | public String getName(){ 26 | return this.name ; 27 | } 28 | 29 | public Integer getCode(){ 30 | return this.code ; 31 | } 32 | 33 | 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/enums/SignStatus.java: -------------------------------------------------------------------------------- 1 | package com.resrun.enums; 2 | 3 | 4 | /** 5 | * @Description: 签名文件状态枚举 6 | * @Package: com.resrun.enums 7 | * @ClassName: SignStatus 8 | * @copyright 北京资源律动科技有限公司 9 | */ 10 | 11 | public enum SignStatus { 12 | SIGN_STATUS_NOSIGNATURE("PDF文件未发现任何数字签名信息",1), 13 | SIGN_STATUS_ERROR("PDF签名验证时发生错误,签名包含不正确的、无法识别的、已损坏的或可疑的数据",2), 14 | SIGN_STATUS_FIDDLE("PDF中存在数字证书,但文档被篡改,进入下一步,展示验证结果",3), 15 | SIGN_STATUS_RIGHT("PDF中存在数字证书,文档未被篡改,进入下一步,展示验证结果",4), 16 | 17 | ; 18 | 19 | private String msg; 20 | private Integer code; 21 | SignStatus(String msg,Integer code){ 22 | this.msg = msg; 23 | this.code = code; 24 | } 25 | 26 | public Integer getCode(){ 27 | return code; 28 | } 29 | public String getMsg(){ 30 | return msg; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/enums/SignTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.resrun.enums; 2 | 3 | /** 4 | * @Description: 签署方式枚举 5 | * @Package: com.resrun.enums 6 | * @ClassName: SignTypeEnum 7 | * @copyright 北京资源律动科技有限公司 8 | */ 9 | public enum SignTypeEnum { 10 | 11 | 12 | POSITION(1,"位置签署"), 13 | KEYWORD(2,"关键字签署"), 14 | 15 | ; 16 | 17 | private String msg; 18 | private Integer code; 19 | 20 | 21 | SignTypeEnum(Integer code,String msg){ 22 | this.msg = msg; 23 | this.code = code; 24 | } 25 | 26 | public Integer getCode(){ 27 | return code; 28 | } 29 | public String getMsg(){ 30 | return msg; 31 | } 32 | 33 | 34 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/cert/CertService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.cert; 2 | 3 | import com.resrun.service.pojo.BaseCertificateInfo; 4 | import com.resrun.service.pojo.GenerateCertificateInfo; 5 | import org.bouncycastle.asn1.x500.X500Name; 6 | import org.bouncycastle.asn1.x509.Extension; 7 | import org.bouncycastle.cert.X509CertificateHolder; 8 | import org.bouncycastle.cert.X509v3CertificateBuilder; 9 | import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; 10 | import org.bouncycastle.operator.ContentSigner; 11 | import org.bouncycastle.operator.OperatorCreationException; 12 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.io.*; 16 | import java.math.BigInteger; 17 | import java.security.*; 18 | import java.security.cert.Certificate; 19 | import java.security.cert.CertificateException; 20 | import java.security.cert.CertificateFactory; 21 | import java.security.cert.X509Certificate; 22 | import java.util.*; 23 | 24 | /** 25 | * @Description: 证书业务 26 | * @Package: com.resrun.service.cert 27 | * @ClassName: CertService 28 | * @copyright 北京资源律动科技有限公司 29 | */ 30 | @Service 31 | public class CertService { 32 | 33 | 34 | //SHA256WITHRSA 35 | 36 | private String algorithmSignature = "SHA256withRSA"; 37 | 38 | private String password = "123456" ; 39 | 40 | 41 | 42 | /** 43 | * 设置证书签名算法 SHA1withRSA SHA256withRSA 44 | * @param algorithmSignature 45 | */ 46 | public void setAlgorithmSignature(String algorithmSignature){ 47 | this.algorithmSignature = algorithmSignature; 48 | } 49 | 50 | 51 | /** 52 | * 证书类型 53 | * @param lifespan 单位年 54 | * 55 | * @return 56 | */ 57 | public GenerateCertificateInfo generateCertificate(BaseCertificateInfo root, String subject,int lifespan) throws Exception { 58 | X500Name issuer = new X500Name(subject); 59 | KeyStore.PrivateKeyEntry rootPK = null; 60 | PrivateKey signPrivateKey = null; 61 | List chains = new ArrayList<>(5); 62 | if(root != null){ 63 | rootPK = (KeyStore.PrivateKeyEntry) root.getCert().getEntry(root.getAlias(), 64 | new KeyStore.PasswordProtection(root.getPassword().toCharArray())); 65 | X509Certificate rootCert = (X509Certificate) rootPK.getCertificate(); 66 | 67 | issuer = X500Name.getInstance(rootCert.getSubjectX500Principal().getEncoded()); 68 | 69 | signPrivateKey = rootPK.getPrivateKey(); 70 | 71 | Certificate [] rootChains = rootPK.getCertificateChain(); 72 | for (int i=0;i extensions) throws IOException, CertificateException, OperatorCreationException { 124 | 125 | X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( 126 | issuer, serial, startDate, expireDate, 127 | subject, publicKey); 128 | ContentSigner sigGen = new JcaContentSignerBuilder(algorithmSignature).build(privKey); 129 | //privKey是CA的私钥,publicKey是待签名的公钥,那么生成的证书就是被CA签名的证书。 130 | if (extensions != null){ 131 | for (Extension ext : extensions) { 132 | builder.addExtension(ext.getExtnId(), ext.isCritical(),ext.getExtnValue()); 133 | } 134 | } 135 | X509CertificateHolder holder = builder.build(sigGen); 136 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 137 | InputStream is1 = new ByteArrayInputStream(holder.toASN1Structure() 138 | .getEncoded()); 139 | X509Certificate theCert = (X509Certificate) cf.generateCertificate(is1); 140 | is1.close(); 141 | return theCert; 142 | } 143 | 144 | 145 | // public static void main(String[] args) throws Exception { 146 | //// 147 | // KeyStore store = loadJKS("Ting111"); 148 | // BaseCertificateInfo baseCertificateInfo = new BaseCertificateInfo(); 149 | // baseCertificateInfo.setAlias("root"); 150 | // baseCertificateInfo.setCert(store); 151 | // baseCertificateInfo.setPassword("123456"); 152 | // String x500Name = "CN=Ting222,OU=研发部,O=资源律动,L=BeiJing,ST=BeiJing,C=CN"; 153 | // 154 | // GenerateCertificateInfo certificateInfo = GenerateRootCertificate.instance(x500Name, CertificateType.RSA).generateCertificate(baseCertificateInfo,"123456",10); 155 | // FileUtils.writeByteArrayToFile(new File("C:\\Users\\Administrator\\Desktop\\tem\\cert\\Ting222.jks"), certificateInfo.getJks()); 156 | // 157 | // } 158 | 159 | private static BigInteger str2BigInteger(String str){ 160 | StringBuffer sb = new StringBuffer(); 161 | //将字符串转换为字符数组 162 | char ch[] = str.toCharArray(); 163 | for(int i = 0; i < ch.length; i++) { 164 | String hexString = Integer.toHexString(ch[i]); 165 | sb.append(hexString); 166 | } 167 | return new BigInteger(sb.toString()); 168 | } 169 | 170 | private static String big2String(String str){ 171 | String result = new String(); 172 | char[] charArray = str.toString().toCharArray(); 173 | for(int i = 0; i < charArray.length; i=i+2) { 174 | String st = ""+charArray[i]+""+charArray[i+1]; 175 | char ch1 = (char)Integer.parseInt(st, 16); 176 | result = result + ch1; 177 | } 178 | return result; 179 | } 180 | 181 | public byte [] coverToPfx(byte [] jks, String password) { 182 | try { 183 | 184 | KeyStore inputKeyStore = KeyStore.getInstance("JKS"); 185 | ByteArrayInputStream inputStream = new ByteArrayInputStream(jks); 186 | inputKeyStore.load(inputStream, password.toCharArray()); 187 | 188 | KeyStore outputKeyStore = KeyStore.getInstance("PKCS12"); 189 | 190 | outputKeyStore.load(null, password.toCharArray()); 191 | 192 | Enumeration enums = inputKeyStore.aliases(); 193 | 194 | while (enums.hasMoreElements()) { // we are readin just one certificate. 195 | 196 | String keyAlias = (String) enums.nextElement(); 197 | 198 | if (inputKeyStore.isKeyEntry(keyAlias)) { 199 | Key key = inputKeyStore.getKey(keyAlias, password.toCharArray()); 200 | Certificate[] certChain = inputKeyStore 201 | .getCertificateChain(keyAlias); 202 | 203 | outputKeyStore.setKeyEntry("", key, password 204 | .toCharArray(), certChain); 205 | } 206 | } 207 | 208 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 209 | outputKeyStore.store(out, password.toCharArray()); 210 | out.close(); 211 | return out.toByteArray(); 212 | } catch (Exception e) { 213 | e.printStackTrace(); 214 | } 215 | return null; 216 | } 217 | 218 | 219 | public static KeyStore loadJKS(String name) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { 220 | KeyStore store = KeyStore.getInstance("JKS"); 221 | File file = new File(""+name+".jks"); 222 | store.load(new FileInputStream(file), "123456".toCharArray()); 223 | return store; 224 | } 225 | 226 | 227 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/image/DateSealService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.image; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import javax.imageio.ImageIO; 6 | import java.awt.*; 7 | import java.awt.image.BufferedImage; 8 | import java.io.ByteArrayOutputStream; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | /** 13 | * @Description: 日期签章业务 14 | * @Package: com.resrun.service.image 15 | * @ClassName: DateSealService 16 | * @copyright 北京资源律动科技有限公司 17 | */ 18 | @Service 19 | public class DateSealService { 20 | 21 | 22 | /** 23 | * @Description #生成日期签章 24 | * @Param date 日期值,可以为空,如果为空则会使用format生成当前时间的日期签章 25 | * format 日期格式, 26 | * width 签章宽, 27 | * height签章高 28 | * @return byte[] 29 | **/ 30 | public byte[] crateDateSeal(String date ,String format, int width, int height){ 31 | byte[] seal = null ; 32 | String value = null ; 33 | if(date != null && date.length() > 0){ 34 | value = date ; 35 | 36 | }else if(format != null && format.length() > 0) { 37 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); 38 | value = simpleDateFormat.format(new Date()); 39 | } 40 | if(value == null){ 41 | return null ; 42 | } 43 | 44 | // 设置背景宽高 45 | BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 46 | // 获取图形上下文对象 47 | Graphics2D graphics = image.createGraphics(); 48 | graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); 49 | // 填充 50 | graphics.fillRect(0, 0, width, height); 51 | // 设定字体大小及样式 52 | graphics.setFont(new Font("宋体", Font.BOLD,18)); 53 | // 字体颜色 54 | graphics.setColor(Color.BLACK); 55 | // 描绘字符串 56 | graphics.drawString(value, 20, 28); 57 | graphics.dispose(); 58 | if(image == null){ 59 | return seal ; 60 | } 61 | try { 62 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 63 | ImageIO.write(image,"png",byteArrayOutputStream); 64 | seal = byteArrayOutputStream.toByteArray(); 65 | } catch (Exception e) { 66 | e.printStackTrace(); 67 | } 68 | return seal ; 69 | } 70 | 71 | 72 | 73 | 74 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/image/EntSealClipService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.image; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import javax.imageio.ImageIO; 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.image.BufferedImage; 9 | import java.io.ByteArrayInputStream; 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | 13 | /** 14 | * @Description: 企业签章业务 15 | * @Package: com.resrun.service.image 16 | * @ClassName: EntSealClipService 17 | * @copyright 北京资源律动科技有限公司 18 | */ 19 | @Service 20 | public class EntSealClipService { 21 | 22 | public byte[] clip(byte[] originByte, Integer colorRange){ 23 | ByteArrayInputStream inputStream = new ByteArrayInputStream(originByte); 24 | BufferedImage image = null; 25 | try { 26 | image = ImageIO.read(inputStream); 27 | } catch (Exception e) { 28 | e.printStackTrace(); 29 | } 30 | // 高度和宽度 31 | int height = image.getHeight(); 32 | int width = image.getWidth(); 33 | 34 | // 生产背景透明和内容透明的图片 35 | ImageIcon imageIcon = new ImageIcon(image); 36 | BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); 37 | Graphics2D g2D = (Graphics2D) bufferedImage.getGraphics(); // 获取画笔 38 | g2D.drawImage(imageIcon.getImage(), 0, 0, null); // 绘制Image的图片,使用了imageIcon.getImage(),目的就是得到image,直接使用image就可以的 39 | 40 | int alpha = 0; // 图片透明度 41 | // 外层遍历是Y轴的像素 42 | for (int y = bufferedImage.getMinY(); y < bufferedImage.getHeight(); y++) { 43 | // 内层遍历是X轴的像素 44 | for (int x = bufferedImage.getMinX(); x < bufferedImage.getWidth(); x++) { 45 | int rgb = bufferedImage.getRGB(x, y); 46 | // 对当前颜色判断是否在指定区间内 47 | if (colorInRange(rgb,colorRange)){ 48 | // #AARRGGBB 最前两位为透明度 49 | rgb = (alpha << 24) | (rgb & 0x00ffffff); 50 | bufferedImage.setRGB(x, y, rgb); 51 | alpha = 0; 52 | }else{ 53 | // 设置为不透明 54 | alpha = 255; 55 | // #AARRGGBB 最前两位为透明度 56 | rgb = (alpha << 24) | (0xff0000); 57 | bufferedImage.setRGB(x, y, rgb); 58 | } 59 | 60 | } 61 | } 62 | // 绘制设置了RGB的新图片,这一步感觉不用也可以只是透明地方的深浅有变化而已,就像蒙了两层的感觉 63 | g2D.drawImage(bufferedImage, 0, 0, null); 64 | 65 | // 生成图片为PNG 66 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 67 | try { 68 | ImageIO.write(bufferedImage, "png", outputStream); 69 | } catch (IOException e) { 70 | e.printStackTrace(); 71 | } 72 | return outputStream.toByteArray(); 73 | } 74 | 75 | // 判断是背景还是内容 76 | public boolean colorInRange(int color , Integer colorRange) { 77 | int red = (color & 0xff0000) >> 16;// 获取color(RGB)中R位 78 | int green = (color & 0x00ff00) >> 8;// 获取color(RGB)中G位 79 | int blue = (color & 0x0000ff);// 获取color(RGB)中B位 80 | // 通过RGB三分量来判断当前颜色是否在指定的颜色区间内 81 | if (red >= colorRange && green >= colorRange && blue >= colorRange){ 82 | return true; 83 | } 84 | return false; 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/image/EntSealGenerateService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.image; 2 | 3 | import com.resrun.utils.Base64; 4 | import org.apache.pdfbox.io.IOUtils; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.imageio.ImageIO; 8 | import java.awt.*; 9 | import java.awt.image.BufferedImage; 10 | import java.io.*; 11 | 12 | /** 13 | * @Description: 企业签章业务 14 | * @Package: com.resrun.service.image 15 | * @ClassName: EntSealGenerateService 16 | * @copyright 北京资源律动科技有限公司 17 | */ 18 | @Service 19 | public class EntSealGenerateService { 20 | 21 | 22 | 23 | /** 24 | * @Description #生成企业签章 25 | * @Param [topText, middleText] 26 | * @return byte[] 27 | **/ 28 | public byte[] generateEntSeal(String topText,String middleText){ 29 | 30 | byte[] bytes = export2pic("png", middleText,topText); 31 | return bytes; 32 | } 33 | 34 | 35 | 36 | 37 | /** 38 | * 印章名称距中心点偏移量,按照y轴方向 39 | */ 40 | private int nameOffset = 50; 41 | /** 42 | * 印章宽度 43 | */ 44 | private int width = 200; 45 | /** 46 | * 印章高度 47 | */ 48 | private int height = 200; 49 | /** 50 | * 印章中心标志(默认为五角星)外接圆半径 51 | */ 52 | private float radius = 30; 53 | /** 54 | * 印章名称颜色 55 | */ 56 | private Color nameColor = Color.RED; 57 | /** 58 | * 印章所属单位 59 | */ 60 | // private String firm; 61 | /** 62 | * 印章所属单位颜色 63 | */ 64 | private Color firmColor = Color.RED; 65 | private float firmScale = 0.7F; 66 | /** 67 | * 边框线宽 68 | */ 69 | private float borderWidth = 5F; 70 | /** 71 | * 边框颜色 72 | */ 73 | private Color borderColor = Color.RED; 74 | /** 75 | * 印章标记(默认为五角星)线宽 76 | */ 77 | private float signBorderWidth = 3F; 78 | /** 79 | * 印章标记颜色 80 | */ 81 | private Color signBorderColor = Color.RED; 82 | /** 83 | * 印章标记填充颜色 84 | */ 85 | private Color signFillColor = Color.RED; 86 | 87 | 88 | public void draw(Graphics2D g2d,String middleText,String topText) { 89 | // 把绘制起点挪到圆中心点 90 | g2d.translate(width / 2, height / 2); 91 | 92 | Stroke stroke = g2d.getStroke();// 旧的线性 93 | // 填充五角星 94 | Polygon polygon = getPentaclePoints(radius); 95 | if (signFillColor != null) { 96 | g2d.setColor(signFillColor); 97 | g2d.fill(polygon); 98 | } 99 | // 绘制五角星边框 100 | g2d.setStroke(new BasicStroke(signBorderWidth)); 101 | g2d.setColor(signBorderColor); 102 | g2d.draw(polygon); 103 | 104 | // 绘制印章边框 105 | g2d.setFont(nameFont); 106 | g2d.setColor(borderColor); 107 | g2d.setStroke(new BasicStroke(borderWidth)); 108 | g2d.drawOval(-width / 2, -height / 2, width, height); 109 | g2d.setStroke(stroke); 110 | 111 | // 绘制印章名称 112 | g2d.setFont(nameFont); 113 | g2d.setColor(nameColor); //g2d.setStroke(new BasicStroke(10F)); 114 | FontMetrics fm = g2d.getFontMetrics(); 115 | int w = fm.stringWidth(middleText);// 名称宽度 116 | int h = fm.getHeight();// 名称高度 117 | int y = fm.getAscent() - h / 2;// 求得中心线经过字体的高度的一半时的字体的起绘点 118 | g2d.drawString(middleText, -w / 2, y + nameOffset); 119 | 120 | // 绘制印章单位 121 | g2d.setFont(getFirmFont(topText)); 122 | g2d.setColor(firmColor); 123 | fm = g2d.getFontMetrics(); 124 | h = fm.getHeight();// 字高度 125 | 126 | int count = topText.length();// 字数 127 | int r = width / 2;// 半径,就假设此印章是个矩形,方便计算 128 | float angle; 129 | float start; 130 | 131 | if(count>1 && count <=10){ 132 | angle = 20f;// 字间角度 133 | start = 90+(360 - angle*(count-1))/2;// 以x轴正向为0,顺时针旋转 134 | }else if(count > 10){ 135 | angle = (360 - firmAngle) / (count-1);// 字间角度 136 | start = 90+(360 - angle*(count-1))/2;// 以x轴正向为0,顺时针旋转 137 | }else{ 138 | angle = 0f;// 字间角度 139 | start = 90+(360 - angle*(2-1))/2;// 以x轴正向为0,顺时针旋转 140 | } 141 | double vr = Math.toRadians(90);// 垂直旋转弧度 142 | char[] chars = topText.toCharArray(); 143 | for (int i = 0; i < count; i++) { 144 | char c = chars[i];// 需要绘制的字符 145 | int cw = fm.charWidth(c);// 此字符宽度 146 | float a = start + angle * i;// 现在角度 147 | double radians = Math.toRadians(a); 148 | g2d.rotate(radians);// 旋转坐标系,让要绘制的字符处于x正轴 149 | float x = r - h;// 绘制字符的x坐标为半径减去字高度 150 | g2d.translate(x, 0);// 移动到此位置,此时字和x轴垂直 151 | g2d.rotate(vr);// 旋转90度,让字平行于x轴 152 | g2d.scale(firmScale, 1);// 缩放字体宽度 153 | g2d.drawString(String.valueOf(c), -cw / 2, 0);// 此点为字的中心点 154 | // 将所有设置还原,等待绘制下一个 155 | g2d.scale(1 / firmScale, 1); 156 | g2d.rotate(-vr); 157 | g2d.translate(-x, 0); 158 | g2d.rotate(-radians); 159 | } 160 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 161 | } 162 | 163 | /** 164 | * 获取具有指定半径外接圆的五角星顶点 165 | * 166 | * @param radius 167 | * 圆半径 168 | */ 169 | private Polygon getPentaclePoints(float radius) { 170 | if (radius <= 0) 171 | return null; 172 | float lradius = radius * 0.381966f;// 根据radius求内圆半径 173 | double halfpi = Math.PI / 180f; 174 | Point[] points = new Point[10]; 175 | for (int i = 0; i < points.length; i++) { 176 | if (i % 2 == 1) 177 | points[i] = new Point( 178 | (int) (Math.sin(halfpi * 36 * i) * radius), 179 | (int) (Math.cos(halfpi * 36 * i) * radius)); 180 | else 181 | points[i] = new Point( 182 | (int) (Math.sin(halfpi * 36 * i) * lradius), 183 | (int) (Math.cos(halfpi * 36 * i) * lradius)); 184 | } 185 | Polygon polygon = new Polygon(); 186 | for (Point p : points) { 187 | polygon.addPoint(p.x, p.y); 188 | } 189 | return polygon; 190 | } 191 | 192 | public int getWidth() { 193 | return width; 194 | } 195 | 196 | public int getHeight() { 197 | return height; 198 | } 199 | 200 | 201 | private Font nameFont = new Font("宋体", Font.PLAIN, 19); 202 | 203 | private Font getFirmFont(String topText){ 204 | Font font = null; 205 | int len = topText.length(); 206 | System.out.println(len); 207 | if(len==1){ 208 | setFirmAngle(355); 209 | font = new Font("宋体", Font.PLAIN, 25); 210 | }else if(len>1 && len<=3){ 211 | setFirmAngle(310); 212 | font = new Font("宋体", Font.PLAIN, 25); 213 | }else if(len>3 && len<=6){ 214 | setFirmAngle(250); 215 | font = new Font("宋体", Font.PLAIN, 25); 216 | }else if(len>6 && len <=10){ 217 | setFirmAngle(200); 218 | font = new Font("宋体", Font.PLAIN, 25); 219 | }else if(len>10 && len<=13){ 220 | setFirmAngle(180); 221 | font = new Font("宋体", Font.PLAIN, 25); 222 | } 223 | else if(len>13 && len<=20){ 224 | font = new Font("宋体", Font.PLAIN, 25); 225 | setFirmAngle(120); 226 | }else if(len>20 && len <= 25){ 227 | font = new Font("宋体", Font.PLAIN, 23); 228 | 229 | setFirmAngle(80); 230 | }else if(len>25 && len < 30){ 231 | setFirmAngle(80); 232 | font = new Font("宋体", Font.PLAIN, 19); 233 | }else if(len>=30 && len <= 40){ 234 | setFirmAngle(80); 235 | font = new Font("宋体", Font.PLAIN, 19); 236 | }else{ 237 | setFirmAngle(10); 238 | font = new Font("宋体", Font.PLAIN, 17); 239 | } 240 | return font; 241 | } 242 | 243 | 244 | 245 | private int firmAngle; 246 | public void setFirmAngle(int firmAngle){ 247 | this.firmAngle = firmAngle; 248 | } 249 | 250 | 251 | 252 | 253 | 254 | /** 255 | * 导出此印章为透明背景的图片字节数组. 256 | * 257 | * @param format 258 | * 图片类型,如果为null,则默认为png 259 | * @return 数组 260 | * @throws FileNotFoundException 261 | * @throws IOException 262 | * 写出图像数据出现问题 263 | */ 264 | public byte[] export2pic(String format,String middleText,String topText) { 265 | int fix = 5;// 宽高修正,如果宽高就为图片宽高,可能边框线被切割 266 | BufferedImage bi = new BufferedImage(getWidth() + fix * 2, getHeight() 267 | + fix * 2, 3); 268 | 269 | 270 | Graphics2D g2d = bi.createGraphics(); 271 | //防锯齿状毛刺算法 272 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 273 | g2d.translate(fix, fix); 274 | draw(g2d,middleText,topText); 275 | 276 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 277 | try { 278 | ImageIO.write(bi, format == null ? "png" : format, baos); 279 | return baos.toByteArray(); 280 | } catch (IOException e) { 281 | throw new RuntimeException(topText+":生成企业签章失败",e); 282 | }finally { 283 | try { 284 | if(baos!=null) 285 | baos.close(); 286 | } catch (IOException e) { 287 | } 288 | } 289 | 290 | } 291 | 292 | 293 | 294 | 295 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/image/PdfConvertImageService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.image; 2 | 3 | import com.resrun.service.pojo.ConvertImage; 4 | import org.apache.pdfbox.pdmodel.PDDocument; 5 | import org.apache.pdfbox.rendering.PDFRenderer; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.imageio.ImageIO; 9 | import java.awt.image.BufferedImage; 10 | import java.io.ByteArrayOutputStream; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * @Description: pdf文件转图片业务 16 | * @Package: com.resrun.service.image 17 | * @ClassName: Pdf2ImageService 18 | * @copyright 北京资源律动科技有限公司 19 | */ 20 | @Service 21 | public class PdfConvertImageService { 22 | 23 | private Float resolution = 90f ; 24 | /**图片格式为jpg类型**/ 25 | public static final String IMG_JPE = "jpg"; 26 | /**图片格式为png类型**/ 27 | public static final String IMG_PNG = "png"; 28 | 29 | public List convertImage(byte[] signFileByte){ 30 | if(signFileByte == null){ 31 | return null ; 32 | } 33 | List responseList = new ArrayList<>(); 34 | PDDocument doc = null; 35 | try { 36 | //加载pdf文件 37 | doc = PDDocument.load(signFileByte); 38 | PDFRenderer renderer = new PDFRenderer(doc); 39 | int pageCount = doc.getNumberOfPages(); 40 | if(pageCount == 0){ 41 | return responseList; 42 | } 43 | 44 | for (int i = 0; i < pageCount; ++i) { 45 | int page = i ; 46 | BufferedImage image = renderer.renderImageWithDPI(page, resolution); 47 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 48 | ImageIO.write(image, IMG_PNG, stream); 49 | byte[] bytes = stream.toByteArray(); 50 | if(bytes != null){ 51 | ConvertImage response = new ConvertImage(); 52 | response.setImageByte(bytes); 53 | response.setPage(page); 54 | responseList.add(response); 55 | 56 | } 57 | } 58 | }catch (Exception e) { 59 | e.printStackTrace(); 60 | } 61 | 62 | return responseList ; 63 | } 64 | 65 | 66 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/image/PersonalSealService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.image; 2 | 3 | import com.resrun.enums.SealColorEnum; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.awt.*; 7 | import java.awt.image.BufferedImage; 8 | 9 | /** 10 | * @Description: 个人签章业务 11 | * @Package: com.resrun.service.image 12 | * @ClassName: PersonalSealService 13 | * @copyright 北京资源律动科技有限公司 14 | */ 15 | @Service 16 | public class PersonalSealService { 17 | 18 | 19 | private static final int WIDTH = 200;// 图片宽度 20 | private static final int HEIGHT = 200;// 图片高度 21 | 22 | 23 | 24 | 25 | 26 | 27 | /** 28 | * @Description #生成正方形带方框的签章图片 29 | * @Param [name] 30 | * @return java.awt.image.BufferedImage 31 | **/ 32 | public static BufferedImage drawSquareFrame(String name, Integer color) throws Exception { 33 | 34 | int imageWidth = 166 ; 35 | int imageHeight = 166 ; 36 | Color allColor = Color.RED ; 37 | if(SealColorEnum.RED.getCode().equals(color)){ 38 | allColor = Color.RED ; 39 | }else if(SealColorEnum.BLUE.getCode().equals(color)){ 40 | allColor = Color.BLUE; 41 | }else if(SealColorEnum.BLACK.getCode().equals(color)){ 42 | allColor = Color.BLACK; 43 | } 44 | 45 | // 生成正方形图片,宽高都是166,imageType为3是生成透明图片 46 | BufferedImage bi = new BufferedImage(imageWidth, imageHeight, 3); 47 | // 设置属性 48 | Graphics2D g2d = bi.createGraphics(); 49 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 50 | // g2d.drawImage(bi, 0, 0, imageWidth, imageHeight, Color.WHITE,null);不能是白色,不然图片就不透明了 51 | g2d.drawImage(bi, 0, 0, imageWidth, imageHeight,null); 52 | 53 | // 线宽 54 | g2d.setStroke(new BasicStroke(8F)); 55 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 56 | g2d.setColor(allColor); 57 | g2d.drawRect(1, 1, imageWidth - 3 , imageHeight - 3); 58 | 59 | 60 | char[] charArray = name.toCharArray(); 61 | g2d.setFont(new Font("楷体", Font.LAYOUT_LEFT_TO_RIGHT, 70));// 写入签名 62 | g2d.setColor(allColor); 63 | if (charArray.length == 3) { 64 | // 设置字体 65 | g2d.drawString(new String(new char[] { charArray[0] }), 80, 68); // 写文字 66 | g2d.drawString(new String(new char[] { charArray[1] }), 80, 148); // 写文字 67 | g2d.drawString(new String(new char[] { charArray[2] }), 5, 108); // 写文字 68 | 69 | } else { 70 | // 设置字体 71 | g2d.drawString(new String(new char[] { charArray[0] }), 80, 68); // 写文字 72 | g2d.drawString(new String(new char[] { charArray[1] }), 80, 148); // 写文字 73 | g2d.drawString(new String(new char[] { charArray[2] }), 5, 68); // 写文字 74 | g2d.drawString(new String(new char[] { charArray[3] }), 5, 148); // 写文字 75 | } 76 | 77 | 78 | return bi; 79 | 80 | 81 | } 82 | 83 | /** 84 | * @Description #生成正方形不带方框的签章图片 85 | * @Param [name] 86 | * @return java.awt.image.BufferedImage 87 | **/ 88 | public static BufferedImage drawSquareNoFrame(String name,Integer color) throws Exception { 89 | 90 | int imageWidth = 166 ; 91 | int imageHeight = 166 ; 92 | Color allColor = Color.RED ; 93 | if(SealColorEnum.RED.getCode().equals(color)){ 94 | allColor = Color.RED ; 95 | }else if(SealColorEnum.BLUE.getCode().equals(color)){ 96 | allColor = Color.BLUE; 97 | }else if(SealColorEnum.BLACK.getCode().equals(color)){ 98 | allColor = Color.BLACK; 99 | } 100 | 101 | // 生成正方形图片,宽高都是166,imageType为3是生成透明图片 102 | BufferedImage bi = new BufferedImage(imageWidth, imageHeight, 3); 103 | // 设置属性 104 | Graphics2D g2d = bi.createGraphics(); 105 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 106 | // g2d.drawImage(bi, 0, 0, imageWidth, imageHeight, Color.WHITE,null);不能是白色,不然图片就不透明了 107 | g2d.drawImage(bi, 0, 0, imageWidth, imageHeight,null); 108 | 109 | char[] charArray = name.toCharArray(); 110 | g2d.setFont(new Font("楷体", Font.LAYOUT_LEFT_TO_RIGHT, 70));// 写入签名 111 | g2d.setColor(allColor); 112 | if (charArray.length == 3) { 113 | // 设置字体 114 | g2d.drawString(new String(new char[] { charArray[0] }), 80, 68); // 写文字 115 | g2d.drawString(new String(new char[] { charArray[1] }), 80, 148); // 写文字 116 | // 设置字体 117 | g2d.drawString(new String(new char[] { charArray[2] }), 5, 108); // 写文字 118 | } else { 119 | // 设置字体 120 | g2d.drawString(new String(new char[] { charArray[0] }), 80, 68); // 写文字 121 | g2d.drawString(new String(new char[] { charArray[1] }), 80, 148); // 写文字 122 | g2d.drawString(new String(new char[] { charArray[2] }), 5, 68); // 写文字 123 | g2d.drawString(new String(new char[] { charArray[3] }), 5, 148); // 写文字 124 | } 125 | 126 | 127 | return bi; 128 | 129 | 130 | } 131 | 132 | /** 133 | * @Description #生成长方形带方框的签章图片 134 | * @Param [name] 135 | * @return java.awt.image.BufferedImage 136 | **/ 137 | public static BufferedImage drawRectangleFrame(String name,Integer color) throws Exception { 138 | 139 | Color allColor = Color.RED ; 140 | if(SealColorEnum.RED.getCode().equals(color)){ 141 | allColor = Color.RED ; 142 | }else if(SealColorEnum.BLUE.getCode().equals(color)){ 143 | allColor = Color.BLUE; 144 | }else if(SealColorEnum.BLACK.getCode().equals(color)){ 145 | allColor = Color.BLACK; 146 | } 147 | 148 | 149 | if (name == null) { 150 | return null; 151 | } 152 | char[] charArray = name.toCharArray(); 153 | if (charArray.length <= 0) { 154 | return null; 155 | } 156 | int length = charArray.length; 157 | int step = 260; 158 | int width = step * length; 159 | // 图片长度 160 | BufferedImage bi = new BufferedImage(width, step, 3); 161 | Graphics2D g2d = bi.createGraphics(); 162 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 163 | // g2d.drawImage(bi, 0, 0, width, step, Color.WHITE,null);不能是白色,不然图片就不透明了 164 | g2d.drawImage(bi, 0, 0, width, step, null); 165 | 166 | // 线宽 167 | g2d.setStroke(new BasicStroke(8F)); 168 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 169 | g2d.setColor(allColor); 170 | g2d.drawRect(1, 1, width - 5, step - 5); 171 | // 设置字体 172 | g2d.setFont(new Font("楷体", Font.LAYOUT_LEFT_TO_RIGHT, 230));// 写入签名 173 | // g2d.scale(0.9F, 1);// 缩放字体宽度 174 | int x = 0; 175 | for (char c : charArray) { 176 | 177 | g2d.drawString(new String(new char[] { c }), x, 210); // 写文字 178 | x = x + step; 179 | } 180 | return bi; 181 | } 182 | 183 | /** 184 | * @Description #生成长方形不带方框的签章图片 185 | * @Param [name] 186 | * @return java.awt.image.BufferedImage 187 | **/ 188 | public static BufferedImage drawRectangleNoFrame(String name,Integer color) throws Exception { 189 | 190 | if (name == null) { 191 | return null; 192 | } 193 | char[] charArray = name.toCharArray(); 194 | if (charArray.length <= 0) { 195 | return null; 196 | } 197 | Color allColor = Color.RED ; 198 | if(SealColorEnum.RED.getCode().equals(color)){ 199 | allColor = Color.RED ; 200 | }else if(SealColorEnum.BLUE.getCode().equals(color)){ 201 | allColor = Color.BLUE; 202 | }else if(SealColorEnum.BLACK.getCode().equals(color)){ 203 | allColor = Color.BLACK; 204 | } 205 | int length = charArray.length; 206 | int step = 260; 207 | int width = step * length; 208 | // 图片长度 209 | BufferedImage bi = new BufferedImage(width, step, 3); 210 | Graphics2D g2d = bi.createGraphics(); 211 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 212 | //绘制图片 213 | // g2d.drawImage(bi, 0, 0, width, step, Color.WHITE,null); 不能是白色,不然图片就不透明了 214 | g2d.drawImage(bi, 0, 0, width, step, null); 215 | 216 | 217 | // 设置字体 218 | g2d.setFont(new Font("楷体", Font.LAYOUT_LEFT_TO_RIGHT, 230)); 219 | g2d.setColor(allColor); 220 | int x = 0; 221 | for (char c : charArray) { 222 | 223 | g2d.drawString(new String(new char[] { c }), x, 210); // 写文字 224 | x = x + step; 225 | } 226 | return bi; 227 | } 228 | 229 | 230 | 231 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pdf/CalculatePositionService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pdf; 2 | 3 | import com.itextpdf.text.Document; 4 | import com.itextpdf.text.pdf.PdfReader; 5 | import com.resrun.service.pojo.RealPositionProperty; 6 | import com.resrun.service.pojo.SelectKeywords; 7 | import com.resrun.service.pojo.SourcePositionProperty; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * @Description: 计算签署位置业务 16 | * @Package: com.resrun.service.pdf 17 | * @ClassName: CalculatePositionService 18 | * @copyright 北京资源律动科技有限公司 19 | */ 20 | @Service 21 | public class CalculatePositionService { 22 | 23 | /** 24 | * @Description #批量计算真实签署位置 25 | * @Param [sourcePositionProperties] 26 | * @return java.util.List 27 | **/ 28 | public List calculatePositions(List sourcePositionProperties, byte[] pdfFileByte){ 29 | List realPositionProperties = new ArrayList<>(); 30 | 31 | 32 | PdfReader reader = null ; 33 | try { 34 | //将pdf文件读入PdfReader工具类 35 | reader = new PdfReader(pdfFileByte); 36 | for(SourcePositionProperty sourcePositionProperty : sourcePositionProperties){ 37 | RealPositionProperty realPositionProperty = calculatePosition(sourcePositionProperty,pdfFileByte); 38 | Document document = new Document(reader.getPageSize(sourcePositionProperty.getPage())); 39 | //获取真实pdf文件指定页的真实文档宽高 40 | float realPdfHeight = document.getPageSize().getHeight(); 41 | float realPdfWidth = document.getPageSize().getWidth(); 42 | //获取页面上文档的宽高 43 | float sourcePageWidth = sourcePositionProperty.getPageWidth(); 44 | float sourcePageHeight = sourcePositionProperty.getPageHeight(); 45 | //计算真实文档的宽高和页面文档的宽高的比率 46 | float rateHeight = realPdfHeight / sourcePageHeight; 47 | float rateWidth = realPdfWidth / sourcePageWidth; 48 | //计算页面上的横纵坐标,由于页面上给出的是左上角的坐标,所以需要再转换计算一下 49 | //左下角 50 | float pageStartX = sourcePositionProperty.getOffsetX(); 51 | float pageStartY = sourcePositionProperty.getOffsetY() + sourcePositionProperty.getHeight() ; 52 | //右上角 53 | float pageEndX = sourcePositionProperty.getOffsetX() + sourcePositionProperty.getWidth(); 54 | float pageEndY = sourcePositionProperty.getOffsetY(); 55 | //根据比率去计算真实文档上的坐标位置 56 | float startX = pageStartX * rateWidth ; 57 | float startY = pageStartY * rateHeight; 58 | float endX = pageEndX * rateWidth ; 59 | float endY = pageEndY * rateHeight ; 60 | //由于页面的纵坐标和pdf的纵坐标是相反的,所以真实的pdf的纵坐标在计算的时候需要再反转一下 61 | startY = realPdfHeight - startY ; 62 | endY = realPdfHeight - endY ; 63 | //封装返回数据 64 | realPositionProperty.setStartx(startX); 65 | realPositionProperty.setStarty(startY); 66 | realPositionProperty.setEndx(endX); 67 | realPositionProperty.setEndy(endY); 68 | realPositionProperty.setPageNum(sourcePositionProperty.getPage()); 69 | document.close(); 70 | realPositionProperties.add(realPositionProperty); 71 | } 72 | 73 | reader.close(); 74 | } catch (Exception e) { 75 | e.printStackTrace(); 76 | } 77 | return realPositionProperties ; 78 | } 79 | 80 | 81 | 82 | /** 83 | * @Description #单独计算真实签署位置 84 | * @Param [sourcePositionProperty] 85 | * @return com.resrun.modules.sign.service.tool.pojo.RealPositionProperty 86 | **/ 87 | public RealPositionProperty calculatePosition(SourcePositionProperty sourcePositionProperty, byte[] pdfFileByte){ 88 | RealPositionProperty realPositionProperty = new RealPositionProperty(); 89 | PdfReader reader = null ; 90 | Document document = null ; 91 | try { 92 | //将pdf文件读入PdfReader工具类 93 | reader = new PdfReader(pdfFileByte); 94 | document = new Document(reader.getPageSize(sourcePositionProperty.getPage())); 95 | //获取真实pdf文件指定页的真实文档宽高 96 | float realPdfHeight = document.getPageSize().getHeight(); 97 | float realPdfWidth = document.getPageSize().getWidth(); 98 | //获取页面上文档的宽高 99 | float sourcePageWidth = sourcePositionProperty.getPageWidth(); 100 | float sourcePageHeight = sourcePositionProperty.getPageHeight(); 101 | //计算真实文档的宽高和页面文档的宽高的比率 102 | float rateHeight = realPdfHeight / sourcePageHeight; 103 | float rateWidth = realPdfWidth / sourcePageWidth; 104 | //计算页面上的横纵坐标,由于页面上给出的是左上角的坐标,所以需要再转换计算一下 105 | //左下角 106 | float pageStartX = sourcePositionProperty.getOffsetX(); 107 | float pageStartY = sourcePositionProperty.getOffsetY() + sourcePositionProperty.getHeight() ; 108 | //右上角 109 | float pageEndX = sourcePositionProperty.getOffsetX() + sourcePositionProperty.getWidth(); 110 | float pageEndY = sourcePositionProperty.getOffsetY(); 111 | //根据比率去计算真实文档上的坐标位置 112 | float startX = pageStartX * rateWidth ; 113 | float startY = pageStartY * rateHeight; 114 | float endX = pageEndX * rateWidth ; 115 | float endY = pageEndY * rateHeight ; 116 | //由于页面的纵坐标和pdf的纵坐标是相反的,所以真实的pdf的纵坐标在计算的时候需要再反转一下 117 | startY = realPdfHeight - startY ; 118 | endY = realPdfHeight - endY ; 119 | //封装返回数据 120 | realPositionProperty.setStartx(startX); 121 | realPositionProperty.setStarty(startY); 122 | realPositionProperty.setEndx(endX); 123 | realPositionProperty.setEndy(endY); 124 | realPositionProperty.setPageNum(sourcePositionProperty.getPage()); 125 | 126 | document.close(); 127 | reader.close(); 128 | } catch (Exception e) { 129 | e.printStackTrace(); 130 | } 131 | return realPositionProperty ; 132 | } 133 | 134 | 135 | public RealPositionProperty calculatePosition(SourcePositionProperty sourcePositionProperty){ 136 | RealPositionProperty realPositionProperty = new RealPositionProperty(); 137 | //获取真实pdf文件指定页的真实文档宽高 138 | float realPdfHeight = sourcePositionProperty.getRealHeight(); 139 | float realPdfWidth = sourcePositionProperty.getRealWidth(); 140 | //获取页面上文档的宽高 141 | float sourcePageWidth = sourcePositionProperty.getPageWidth(); 142 | float sourcePageHeight = sourcePositionProperty.getPageHeight(); 143 | //计算真实文档的宽高和页面文档的宽高的比率 144 | float rateHeight = realPdfHeight / sourcePageHeight; 145 | float rateWidth = realPdfWidth / sourcePageWidth; 146 | //计算页面上的横纵坐标,由于页面上给出的是左上角的坐标,所以需要再转换计算一下 147 | //左下角 148 | float pageStartX = sourcePositionProperty.getOffsetX(); 149 | float pageStartY = sourcePositionProperty.getOffsetY() + sourcePositionProperty.getHeight() ; 150 | //右上角 151 | float pageEndX = sourcePositionProperty.getOffsetX() + sourcePositionProperty.getWidth(); 152 | float pageEndY = sourcePositionProperty.getOffsetY(); 153 | //根据比率去计算真实文档上的坐标位置 154 | float startX = pageStartX * rateWidth ; 155 | float startY = pageStartY * rateHeight; 156 | float endX = pageEndX * rateWidth ; 157 | float endY = pageEndY * rateHeight ; 158 | //由于页面的纵坐标和pdf的纵坐标是相反的,所以真实的pdf的纵坐标在计算的时候需要再反转一下 159 | startY = realPdfHeight - startY ; 160 | endY = realPdfHeight - endY ; 161 | //封装返回数据 162 | realPositionProperty.setStartx(startX); 163 | realPositionProperty.setStarty(startY); 164 | realPositionProperty.setEndx(endX); 165 | realPositionProperty.setEndy(endY); 166 | realPositionProperty.setPageNum(sourcePositionProperty.getPage()); 167 | return realPositionProperty ; 168 | } 169 | 170 | 171 | 172 | 173 | /** 174 | * 通过查询关键字来获得签名位置信息 175 | * @param pdfFile 签署源文件 176 | * @param keyWords 关键字 177 | * @param width 签章宽度 178 | * @param height 签章高度 179 | * @return 签署位置信息 180 | * @throws IOException 181 | */ 182 | public RealPositionProperty getPositionByKeyWords(byte[] pdfFile, String keyWords, int width, int height) { 183 | RealPositionProperty positionProperty = new RealPositionProperty(); 184 | //调用通过关键字查询位置的方法 185 | float[] result = new float[0]; 186 | try { 187 | result = new SelectKeywords().selectKeyword(pdfFile,keyWords); 188 | } catch (Exception e) { 189 | e.printStackTrace(); 190 | } 191 | if(result !=null){ 192 | 193 | positionProperty.setStartx(result[0]); 194 | positionProperty.setStarty(result[1]+height/4); 195 | positionProperty.setPageNum((int)result[2]); 196 | positionProperty.setEndx(result[0]+width/2); 197 | positionProperty.setEndy(result[1]-height/4); 198 | 199 | } 200 | return positionProperty; 201 | } 202 | 203 | /** 204 | * 通过查询关键字来获得签名位置信息
205 | * 206 | * 同一个关键字出现在多处会一次性全部找出 207 | * 208 | * @param pdfFile 签署源文件 209 | * @param keyWords 关键字 210 | * @param width 签章宽度 211 | * @param height 签章高度 212 | * @return 签署位置信息 213 | * @throws IOException 214 | */ 215 | public List getAllPositionByKeyWords(byte[] pdfFile,String keyWords,int width,int height) { 216 | List positions = new ArrayList(); 217 | //调用通过关键字查询位置的方法 218 | List results = null; 219 | try { 220 | results = new SelectKeywords().selectAllKeyword(pdfFile, keyWords); 221 | } catch (Exception e) { 222 | e.printStackTrace(); 223 | } 224 | if(results !=null && results.size()>0){ 225 | for (float[] result : results) { 226 | RealPositionProperty positionProperty = new RealPositionProperty(); 227 | 228 | positionProperty.setStartx(result[0]); 229 | positionProperty.setStarty(result[1]+height/4); 230 | positionProperty.setPageNum((int)result[2]); 231 | positionProperty.setEndx(result[0]+width/2); 232 | positionProperty.setEndy(result[1]-height/4); 233 | 234 | 235 | positions.add(positionProperty); 236 | } 237 | } 238 | return positions; 239 | } 240 | 241 | 242 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pdf/SignService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pdf; 2 | 3 | import com.itextpdf.text.DocumentException; 4 | import com.itextpdf.text.Image; 5 | import com.itextpdf.text.Rectangle; 6 | import com.itextpdf.text.pdf.PdfReader; 7 | import com.itextpdf.text.pdf.PdfSignatureAppearance; 8 | import com.itextpdf.text.pdf.PdfStamper; 9 | import com.itextpdf.text.pdf.security.*; 10 | import com.resrun.service.pojo.CertificateProperty; 11 | import com.resrun.service.pojo.RealPositionProperty; 12 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.io.ByteArrayInputStream; 16 | import java.io.ByteArrayOutputStream; 17 | import java.io.IOException; 18 | import java.security.GeneralSecurityException; 19 | import java.security.KeyStore; 20 | import java.security.PrivateKey; 21 | import java.security.Security; 22 | import java.security.cert.Certificate; 23 | 24 | /** 25 | * @Description: 签署业务 26 | * @Package: com.resrun.service.pdf 27 | * @ClassName: SignService 28 | * @copyright 北京资源律动科技有限公司 29 | */ 30 | @Service 31 | public class SignService { 32 | 33 | 34 | public byte[] signingContract(byte[] pdfFile, byte[] signBadge, CertificateProperty cert, 35 | RealPositionProperty position) throws GeneralSecurityException, IOException, DocumentException { 36 | System.setProperty("javax.xml.parsers.DocumentBuilderFactory","com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"); 37 | Security.addProvider(new BouncyCastleProvider()); 38 | //1、解析证书 39 | // Java 安全属性文件中指定的默认 keystore 类型;如果不存在此类属性,则返回字符串 "jks"。 PKCS12 40 | KeyStore ks = KeyStore.getInstance(cert.getCertType()); 41 | try { 42 | char[] chars = cert.getPassword().toCharArray(); 43 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cert.getCertFile()); 44 | ks.load(byteArrayInputStream, chars); 45 | } catch (Exception e) { 46 | e.printStackTrace(); 47 | } 48 | // 获取keystore中的所有别名 49 | String alias = (String) ks.aliases().nextElement(); 50 | // 返回:请求的密钥, 入力参数:别名,用于恢复密钥的密码 51 | PrivateKey pk = (PrivateKey) ks.getKey(alias, cert.getPassword().toCharArray()); 52 | // 证书链(按用户证书在前,根证书授权在后的顺序) 53 | Certificate[] chain = ks.getCertificateChain(alias); 54 | 55 | byte[] signedFileByte = null ; 56 | PdfReader reader = null ; 57 | ByteArrayOutputStream signedFile = null ; 58 | PdfStamper stamper = null ; 59 | try { 60 | //2、读取PDF文件 61 | reader = new PdfReader(pdfFile); 62 | signedFile = new ByteArrayOutputStream(); 63 | stamper = PdfStamper.createSignature(reader, signedFile, '\0', null, true); 64 | //3、给签署属性服务 65 | PdfSignatureAppearance appearance = stamper.getSignatureAppearance(); 66 | if (signBadge == null || position == null) { 67 | appearance.setCertificationLevel(certificationLevel); 68 | } else { 69 | int pageNum = 0; 70 | if (inspect) { 71 | //如果检查就会抛出检查异常 72 | pageNum = position.getPageNum(); 73 | if (pageNum == 0) 74 | throw new IllegalArgumentException("Pdf page number must be greater than one....!!!"); 75 | } else { 76 | pageNum = position.getPageNum() <= 0 ? 1 : position.getPageNum(); 77 | } 78 | appearance.setVisibleSignature(new Rectangle(position.getStartx(), position.getStarty(), position.getEndx(), position.getEndy()), pageNum, null); 79 | // 添加签章图片 80 | Image img = Image.getInstance(signBadge); 81 | appearance.setSignatureGraphic(img); 82 | appearance.setImageScale(-1); 83 | appearance.setCertificationLevel(certificationLevel); 84 | appearance.setRenderingMode(renderingMode); 85 | } 86 | appearance.setReason(reason); 87 | appearance.setLocation(location); 88 | //4、调用签署 Creating the signature 89 | ExternalSignature pks = new PrivateKeySignature(pk, hashAlgorithm, BouncyCastleProvider.PROVIDER_NAME); 90 | ExternalDigest digest = new BouncyCastleDigest(); 91 | MakeSignature.signDetached(appearance, digest, pks, chain, null, ocspClient, tsaClient, 0, cryptoStandard); 92 | signedFileByte = signedFile.toByteArray(); 93 | } catch (Exception e){ 94 | e.printStackTrace(); 95 | }finally { 96 | // 关闭流 97 | if (stamper != null) stamper.close(); 98 | if (signedFile != null) signedFile.close(); 99 | if (reader != null) reader.close(); 100 | } 101 | return signedFileByte ; 102 | } 103 | 104 | 105 | 106 | //是否判断校验不校验PDF页码 107 | private boolean inspect = true; 108 | 109 | private int certificationLevel = PdfSignatureAppearance.NOT_CERTIFIED; 110 | 111 | private PdfSignatureAppearance.RenderingMode renderingMode = PdfSignatureAppearance.RenderingMode.GRAPHIC; 112 | 113 | private String hashAlgorithm = DigestAlgorithms.SHA256; 114 | 115 | private MakeSignature.CryptoStandard cryptoStandard = MakeSignature.CryptoStandard.CMS; 116 | 117 | private String reason = "防伪造防篡改数字校验"; //原因 118 | 119 | private String location; //位置 120 | 121 | private TSAClient tsaClient; //时间戳服务 122 | 123 | private OcspClient ocspClient; 124 | 125 | 126 | 127 | 128 | public boolean isInspect() { 129 | return inspect; 130 | } 131 | 132 | public void setInspect(boolean inspect) { 133 | this.inspect = inspect; 134 | } 135 | 136 | public int getCertificationLevel() { 137 | return certificationLevel; 138 | } 139 | 140 | public void setCertificationLevel(int certificationLevel) { 141 | this.certificationLevel = certificationLevel; 142 | } 143 | 144 | public PdfSignatureAppearance.RenderingMode getRenderingMode() { 145 | return renderingMode; 146 | } 147 | 148 | public void setRenderingMode(PdfSignatureAppearance.RenderingMode renderingMode) { 149 | this.renderingMode = renderingMode; 150 | } 151 | 152 | public String getHashAlgorithm() { 153 | return hashAlgorithm; 154 | } 155 | 156 | public void setHashAlgorithm(String hashAlgorithm) { 157 | this.hashAlgorithm = hashAlgorithm; 158 | } 159 | 160 | public MakeSignature.CryptoStandard getCryptoStandard() { 161 | return cryptoStandard; 162 | } 163 | 164 | public void setCryptoStandard(MakeSignature.CryptoStandard cryptoStandard) { 165 | this.cryptoStandard = cryptoStandard; 166 | } 167 | 168 | public String getReason() { 169 | return reason; 170 | } 171 | 172 | public void setReason(String reason) { 173 | this.reason = reason; 174 | } 175 | 176 | public String getLocation() { 177 | return location; 178 | } 179 | 180 | public void setLocation(String location) { 181 | this.location = location; 182 | } 183 | 184 | public TSAClient getTsaClient() { 185 | return tsaClient; 186 | } 187 | 188 | public void setTsaClient(TSAClient tsaClient) { 189 | this.tsaClient = tsaClient; 190 | } 191 | 192 | public OcspClient getOcspClient() { 193 | return ocspClient; 194 | } 195 | 196 | public void setOcspClient(OcspClient ocspClient) { 197 | this.ocspClient = ocspClient; 198 | } 199 | 200 | 201 | 202 | 203 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/BaseCertificateInfo.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import java.security.KeyStore; 4 | 5 | /** 6 | * @Description: 证书文件属性类 7 | * @Package: com.resrun.service.pojo 8 | * @ClassName: CertificateProperty 9 | * @copyright 北京资源律动科技有限公司 10 | */ 11 | public class BaseCertificateInfo { 12 | 13 | /** 14 | * 根证书 15 | */ 16 | private KeyStore cert; 17 | 18 | /** 19 | * 证书别名 20 | */ 21 | private String alias; 22 | 23 | /** 24 | * 证书密码 25 | */ 26 | private String password; 27 | 28 | public KeyStore getCert() { 29 | return cert; 30 | } 31 | 32 | public void setCert(KeyStore cert) { 33 | this.cert = cert; 34 | } 35 | 36 | public String getAlias() { 37 | return alias; 38 | } 39 | 40 | public void setAlias(String alias) { 41 | this.alias = alias; 42 | } 43 | 44 | public String getPassword() { 45 | return password; 46 | } 47 | 48 | public void setPassword(String password) { 49 | this.password = password; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/CertificateProperty.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * @Description: 证书文件属性类 11 | * @Package: com.resrun.service.pojo 12 | * @ClassName: CertificateProperty 13 | * @copyright 北京资源律动科技有限公司 14 | */ 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | @Data 18 | public class CertificateProperty implements Serializable { 19 | 20 | private static final long serialVersionUID = -2073805779543816269L; 21 | 22 | 23 | private byte[] certFile; 24 | /** 证书的类型 比如:PKCS12和jks*/ 25 | private String certType; 26 | /** 证书密码 */ 27 | private String password; 28 | 29 | 30 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/ConvertImage.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * @Description: 图片转换对象 12 | * @Package: com.resrun.service.pojo 13 | * @ClassName: ConvertImage 14 | * @copyright 北京资源律动科技有限公司 15 | */ 16 | @Data 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class ConvertImage implements Serializable { 20 | 21 | 22 | private static final long serialVersionUID = -7285616018724242137L; 23 | 24 | @ApiModelProperty("页码") 25 | private Integer page ; 26 | 27 | @ApiModelProperty("图片数据") 28 | private byte[] imageByte ; 29 | 30 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/GenerateCertificateInfo.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.Date; 8 | 9 | 10 | /** 11 | * @Description: 证书生成对象 12 | * @Package: com.resrun.service.pojo 13 | * @ClassName: GenerateCertificateInfo 14 | * @copyright 北京资源律动科技有限公司 15 | */ 16 | @Data 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class GenerateCertificateInfo { 20 | 21 | /** 22 | * 证书密码 23 | */ 24 | private String password; 25 | 26 | /** 27 | * 证书类型 JKS PFX 28 | */ 29 | private String certFileType; 30 | 31 | /** 32 | * 证书库 33 | */ 34 | private byte [] jks; 35 | 36 | 37 | /** 38 | * 签名证书 39 | */ 40 | private byte [] pfx; 41 | 42 | /** 43 | * 证书序列号 44 | */ 45 | private String serial; 46 | /** 47 | * 证书签名算法 SHA1withRSA SHA256withRSA 48 | */ 49 | private String algorithmSignature; 50 | 51 | /** 52 | * 证书算法类型: RSA、SM2 53 | */ 54 | private String algorithm; 55 | 56 | /** 57 | * 证书有效期起始时间 58 | */ 59 | private Date termOfValidityStartTime; 60 | 61 | /** 62 | * 证书有效期结束时间 63 | */ 64 | private Date termOfValidityEndTime; 65 | 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/RealPositionProperty.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * @Description: 经过计算后的文件签署位置属性类 11 | * @Package: com.resrun.service.pojo 12 | * @ClassName: PositionProperty 13 | * @copyright 北京资源律动科技有限公司 14 | */ 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | @Data 18 | public class RealPositionProperty implements Serializable { 19 | 20 | 21 | private static final long serialVersionUID = 8586984409612483553L; 22 | 23 | /** 签章左下角x坐标 */ 24 | private float startx; 25 | 26 | /** 签章左下角y坐标*/ 27 | private float starty; 28 | 29 | /** 签章右上角x坐标*/ 30 | private float endx; 31 | 32 | /** 签章右上角x坐标*/ 33 | private float endy; 34 | 35 | private int pageNum; 36 | 37 | // 填写值,填写专用 38 | private String value ; 39 | //对齐方式 40 | private String align ; 41 | //字体 42 | private String fontFamily ; 43 | //文字大小 44 | private Integer fontSize ; 45 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/SelectKeywords.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | import org.apache.pdfbox.pdmodel.PDDocument; 6 | import org.apache.pdfbox.text.PDFTextStripper; 7 | import org.apache.pdfbox.text.TextPosition; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.IOException; 11 | import java.text.DecimalFormat; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * @Description: 关键字计算位置 17 | * @Package: com.resrun.service.pojo 18 | * @ClassName: SelectKeywords 19 | * @copyright 北京资源律动科技有限公司 20 | */ 21 | public class SelectKeywords extends PDFTextStripper { 22 | 23 | private static ThreadLocal keyWorkPair = new ThreadLocal(); 24 | 25 | private Log logger = LogFactory.getLog(SelectKeywords.class); 26 | 27 | public SelectKeywords() throws IOException { 28 | super.setSortByPosition(true); 29 | } 30 | 31 | // public static void main(String[] args) throws Exception { 32 | // //selectKeyword 33 | // File file = new File("e:/test/948ad938bab14f4e8a2d843f6dd81d57.pdf"); 34 | // float [] resus = new SelectKeywords().selectKeyword(IOUtils.toByteArray(file), "948ad938bab14f4e8a2d843f6dd81d57");//66 571 35 | // System.out.println(resus[0]+"--"+resus[1]+"---"+resus[2]); 36 | // } 37 | /** 38 | * 查出PDF里所有的关键字 39 | * @param pdfFile 40 | * @param KEY_WORD 41 | * @return 42 | */ 43 | public List selectAllKeyword(byte [] pdfFile, String KEY_WORD) { 44 | keyWorkPair.set(new KeyWorkPair(KEY_WORD.split(","))); 45 | ByteArrayInputStream in = null; 46 | PDDocument document = null; 47 | try { 48 | in = new ByteArrayInputStream(pdfFile); 49 | document = PDDocument.load(in);//加载pdf文件 50 | this.getText(document); 51 | List allResu = getAllResult(); 52 | return allResu; 53 | } catch (Exception e) { 54 | e.printStackTrace(); 55 | }finally{ 56 | try { 57 | if(in!=null) in.close(); 58 | if(document!=null) document.close(); 59 | } catch (IOException e) { 60 | } 61 | } 62 | return null; 63 | } 64 | private List getAllResult(){ 65 | KeyWorkPair pair = keyWorkPair.get(); 66 | if(pair!=null && pair.getResu()!=null){ 67 | keyWorkPair.set(null); 68 | return pair.getAllResu(); 69 | }else{ 70 | keyWorkPair.set(null); 71 | return null; 72 | } 73 | } 74 | /** 75 | * 查出PDF里最后一个关键字 76 | * @param pdfFile 77 | * @param KEY_WORD 78 | * @return 79 | */ 80 | public float [] selectKeyword(byte [] pdfFile,String KEY_WORD) { 81 | keyWorkPair.set(new KeyWorkPair(KEY_WORD.split(","))); 82 | ByteArrayInputStream in = null; 83 | PDDocument document = null; 84 | try { 85 | in = new ByteArrayInputStream(pdfFile); 86 | document = PDDocument.load(in);//加载pdf文件 87 | this.getText(document); 88 | float[] resu = getResult(); 89 | return resu; 90 | } catch (Exception e) { 91 | e.printStackTrace(); 92 | }finally{ 93 | try { 94 | if(in!=null) in.close(); 95 | if(document!=null) document.close(); 96 | } catch (IOException e) { 97 | } 98 | } 99 | return null; 100 | } 101 | 102 | private float[] getResult(){ 103 | KeyWorkPair pair = keyWorkPair.get(); 104 | if(pair!=null && pair.getResu()!=null){ 105 | keyWorkPair.set(null); 106 | return pair.getResu(); 107 | }else{ 108 | keyWorkPair.set(null); 109 | return null; 110 | } 111 | } 112 | 113 | @Override 114 | protected void writeString(String string, List textPositions) throws IOException { 115 | for (TextPosition text : textPositions) { 116 | String tChar = text.toString(); 117 | char c = tChar.charAt(0); 118 | String REGEX = "[,.\\[\\](:;!?)/]"; 119 | lineMatch = matchCharLine(text); 120 | if ((!tChar.matches(REGEX)) && (!Character.isWhitespace(c))) { 121 | if ((!is1stChar) && (lineMatch == true)) { 122 | appendChar(tChar); 123 | } else if (is1stChar == true) { 124 | setWordCoord(text, tChar); 125 | } 126 | } else { 127 | endWord(); 128 | } 129 | } 130 | endWord(); 131 | } 132 | protected void appendChar(String tChar) { 133 | tWord.append(tChar); 134 | is1stChar = false; 135 | } 136 | 137 | /** 138 | * 139 | * %拼接字符串%。 140 | */ 141 | protected void setWordCoord(TextPosition text, String tChar) { 142 | itext = text; 143 | tWord.append("(").append(pageNo).append(")[").append(roundVal(Float.valueOf(text.getXDirAdj()))).append(" : ") 144 | .append(roundVal(Float.valueOf(text.getYDirAdj()))).append("] ").append(tChar); 145 | is1stChar = false; 146 | } 147 | 148 | protected boolean matchCharLine(TextPosition text) { 149 | 150 | Double yVal = roundVal(Float.valueOf(text.getYDirAdj())); 151 | if (yVal.doubleValue() == lastYVal) { 152 | return true; 153 | } 154 | lastYVal = yVal.doubleValue(); 155 | endWord(); 156 | return false; 157 | } 158 | 159 | protected Double roundVal(Float yVal) { 160 | DecimalFormat rounded = new DecimalFormat("0.0'0'"); 161 | Double yValDub = new Double(rounded.format(yVal)); 162 | return yValDub; 163 | } 164 | 165 | protected void endWord() { 166 | // String newWord = tWord.toString().replaceAll("[^\\x00-\\x7F]", 167 | // "");//为了检索速度 使用正则去掉中文 168 | String newWord = tWord.toString();// 去掉正则 可以检索中文 169 | KeyWorkPair pair = keyWorkPair.get(); 170 | 171 | try { 172 | String[] seekA = pair.getSeekA(); 173 | float[] resu = new float[3]; 174 | String sWord = newWord.substring(newWord.lastIndexOf(' ') + 1); 175 | if (!"".equals(sWord)) { 176 | if (sWord.contains(seekA[0])) { 177 | resu[2] = getCurrentPageNo();// (595,842) 178 | resu[0] = (float) (roundVal(Float.valueOf(itext.getXDirAdj())) + 0.0F); 179 | resu[1] = 842.0F - (float) (roundVal(Float.valueOf(itext.getYDirAdj())) + 0.0F); 180 | logger.info("PDF关键字信息:[页数:" + resu[2] + "][X:" + resu[0] + "][Y:" + resu[1] + "]"); 181 | pair.setResu(resu); 182 | pair.addResuList(resu);//把每一次找出的关键字放在一个集合里 183 | keyWorkPair.set(pair); 184 | } 185 | } 186 | } catch (Exception e) { 187 | e.printStackTrace(); 188 | keyWorkPair.set(null); 189 | throw new RuntimeException(); 190 | } 191 | tWord.delete(0, tWord.length()); 192 | is1stChar = true; 193 | } 194 | 195 | private StringBuilder tWord = new StringBuilder(); 196 | private boolean is1stChar = true; 197 | private boolean lineMatch; 198 | private int pageNo = 0; 199 | private double lastYVal; 200 | 201 | private TextPosition itext; 202 | 203 | /** 204 | * 关键字和返回的位置信息类 205 | */ 206 | class KeyWorkPair { 207 | 208 | public KeyWorkPair(String[] seekA) { 209 | super(); 210 | this.seekA = seekA; 211 | } 212 | public KeyWorkPair(String[] seekA, float[] resu) { 213 | super(); 214 | this.seekA = seekA; 215 | this.resu = resu; 216 | } 217 | public KeyWorkPair() { 218 | super(); 219 | } 220 | public String[] getSeekA() { 221 | return seekA; 222 | } 223 | public void setSeekA(String[] seekA) { 224 | this.seekA = seekA; 225 | } 226 | public float[] getResu() { 227 | return resu; 228 | } 229 | public void setResu(float[] resu) { 230 | this.resu = resu; 231 | } 232 | 233 | public void addResuList(float[] resu) { 234 | resuAll.add(resu); 235 | } 236 | public List getAllResu() { 237 | return resuAll; 238 | } 239 | 240 | private String[] seekA; 241 | private float[] resu; 242 | //所有的位置 243 | private List resuAll = new ArrayList<>(); 244 | } 245 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/SignPdfInfoVo.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Date; 11 | import java.util.List; 12 | 13 | /** 14 | * @Description: 数字签名信息 15 | * @Package: com.resrun.service.pojo 16 | * @ClassName: SignPdfInfoVo 17 | * @copyright 北京资源律动科技有限公司 18 | */ 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @ApiModel("数字签名信息返回对象") 23 | public class SignPdfInfoVo { 24 | /** 25 | * PDF名称 26 | */ 27 | @ApiModelProperty(value = "PDF名称") 28 | private String pdfName; 29 | /** 30 | * PDF大小 31 | */ 32 | @ApiModelProperty(value = "PDF大小") 33 | private String pdfSize; 34 | 35 | /** 36 | * 文件签名结果 37 | */ 38 | @ApiModelProperty(value = "文件签名结果") 39 | private Integer pdfSingResult; 40 | 41 | @ApiModelProperty(value = "数字签名详细集合") 42 | private List signatureDetails = new ArrayList<>(); 43 | 44 | public static class SignatureDetail { 45 | /** 46 | * 数字签名名称 47 | */ 48 | @ApiModelProperty(value = "数字签名名称") 49 | public String signName; 50 | /** 51 | * 签名日期时间 52 | */ 53 | @ApiModelProperty(value = "签名日期时间") 54 | public Date signTime; 55 | /** 56 | * 有效期开始时间 57 | */ 58 | @ApiModelProperty(value = "有效期开始时间") 59 | public Date validStartTime; 60 | /** 61 | * 有效期结束时间 62 | */ 63 | @ApiModelProperty(value = "有效期结束时间") 64 | public Date validEndTime; 65 | /** 66 | * 证书名称 67 | */ 68 | @ApiModelProperty(value = "证书名称") 69 | public String certName; 70 | /** 71 | * 证书序列号 72 | */ 73 | @ApiModelProperty(value = "证书序列号") 74 | public String serialNumber; 75 | /** 76 | * 证书公钥 77 | */ 78 | @ApiModelProperty(value = "证书公钥") 79 | public String publicKey; 80 | /** 81 | * 证书格式 82 | */ 83 | @ApiModelProperty(value = "证书格式") 84 | public String pubKeyFormat; 85 | /** 86 | * 证书签名算法 87 | */ 88 | @ApiModelProperty(value = "证书签名算法") 89 | public String sigAlgName; 90 | /** 91 | * 证书颁发者 92 | */ 93 | @ApiModelProperty(value = "证书颁发者") 94 | public String userDnName; 95 | /** 96 | * 是否被篡改 97 | */ 98 | @ApiModelProperty(value = "是否被篡改") 99 | public boolean validate; 100 | 101 | @ApiModelProperty(value = "图片内容") 102 | public String imageContent; 103 | /** 104 | * 图片路径 105 | */ 106 | @ApiModelProperty(value = "图片路径") 107 | public String imagePath; 108 | 109 | /** 110 | * 签名位置 111 | */ 112 | @ApiModelProperty(value = "签名位置") 113 | public String reason; 114 | /** 115 | * 签名类型 116 | */ 117 | @ApiModelProperty(value = "签名类型") 118 | public String location; 119 | 120 | /** 121 | * 签名页面 122 | */ 123 | @ApiModelProperty(value = "签名页面") 124 | public int pageNum; 125 | 126 | /** 127 | * 图片的base64编码 128 | */ 129 | @ApiModelProperty(value = "图片的base64编码") 130 | public String sealBase64; 131 | 132 | /** 133 | * 文件摘要 134 | */ 135 | @ApiModelProperty(value = "文件摘要") 136 | public String fileDigest; 137 | 138 | public float llx, lly, urx, ury; 139 | /** 140 | * 文件路径 141 | */ 142 | // public String filePath; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/pojo/SourcePositionProperty.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.pojo; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * @Description: 原始文件签署位置属性 12 | * @Package: com.resrun.service.pojo 13 | * @ClassName: SourcePositionProperty 14 | * @copyright 北京资源律动科技有限公司 15 | */ 16 | @Data 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class SourcePositionProperty implements Serializable { 20 | 21 | private static final long serialVersionUID = 725976764583634367L; 22 | 23 | @ApiModelProperty("控件X坐标(左上角)") 24 | private Float offsetX ; 25 | @ApiModelProperty("控件Y坐标(左上角)") 26 | private Float offsetY ; 27 | @ApiModelProperty("控件宽度") 28 | private Float width ; 29 | @ApiModelProperty("控件高度") 30 | private Float height ; 31 | @ApiModelProperty("当前文件页面宽度") 32 | private Float pageWidth ; 33 | @ApiModelProperty("当前文件页面高度") 34 | private Float pageHeight ; 35 | @ApiModelProperty("控件所属页码") 36 | private Integer page ; 37 | 38 | @ApiModelProperty("当前文件页面宽度") 39 | private Float realWidth ; 40 | @ApiModelProperty("当前文件页面高度") 41 | private Float realHeight ; 42 | 43 | 44 | } -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/verify/ExtImageRenderListener.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.verify; 2 | 3 | import com.itextpdf.text.pdf.PdfDictionary; 4 | import com.itextpdf.text.pdf.parser.ImageRenderInfo; 5 | import com.itextpdf.text.pdf.parser.PdfImageObject; 6 | import com.itextpdf.text.pdf.parser.RenderListener; 7 | import com.itextpdf.text.pdf.parser.TextRenderInfo; 8 | import lombok.Data; 9 | import lombok.SneakyThrows; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * @Description: 图像监听类 17 | * @Package: com.resrun.service.verify 18 | * @ClassName: ExtImageRenderListener 19 | * @copyright 北京资源律动科技有限公司 20 | */ 21 | @Slf4j 22 | @Data 23 | public class ExtImageRenderListener implements RenderListener { 24 | private int i; 25 | private String basePath; 26 | 27 | private byte [] seal; 28 | @Override 29 | public void beginTextBlock() { 30 | 31 | } 32 | 33 | public void setBasePath(String basePath) { 34 | if (basePath.endsWith("/")) { 35 | this.basePath = basePath; 36 | } else { 37 | this.basePath = basePath + "/"; 38 | } 39 | } 40 | 41 | @Override 42 | public void renderText(TextRenderInfo renderInfo) { 43 | } 44 | 45 | @Override 46 | public void endTextBlock() { 47 | } 48 | PdfDictionary resources; 49 | 50 | List> numbers = new ArrayList<>(); 51 | 52 | 53 | @SneakyThrows 54 | @Override 55 | public void renderImage(ImageRenderInfo renderInfo) { 56 | PdfImageObject image = renderInfo.getImage(); 57 | if (image == null) { 58 | log.debug("Image {} could not be read", renderInfo.getRef().getNumber()); 59 | return; 60 | } 61 | seal = image.getImageAsBytes(); 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/verify/SignVerifyService.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.verify; 2 | 3 | 4 | import com.resrun.service.pojo.SignPdfInfoVo; 5 | import com.resrun.controller.vo.response.VerifyResponse; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | 9 | 10 | /** 11 | * @Description: 在线签名验签服务实现类 12 | * @Package: com.resrun.service.verify 13 | * @ClassName: SignVerifyServiceImpl 14 | * @copyright 北京资源律动科技有限公司 15 | */ 16 | @Slf4j 17 | @Service 18 | public class SignVerifyService{ 19 | 20 | /** 21 | * 获取pdf签名图片信息 22 | * @return 提取结果 23 | */ 24 | public VerifyResponse getImageFromPdf(byte[] bytes, String fileName) { 25 | VerifyResponse response = new VerifyResponse(); 26 | SignPdfInfoVo signPdfInfo = new SignPdfInfoVo(); 27 | 28 | try { 29 | 30 | signPdfInfo = VerifySign.getSignFromPdf(bytes); 31 | signPdfInfo.setPdfName(fileName); 32 | signPdfInfo.setPdfSize(String.format("%.2f",bytes.length/1024.0)); 33 | switch (signPdfInfo.getPdfSingResult()) { 34 | case 1: 35 | return new VerifyResponse(com.resrun.enums.SignStatus.SIGN_STATUS_NOSIGNATURE.getMsg(),signPdfInfo); 36 | case 2: 37 | return new VerifyResponse(com.resrun.enums.SignStatus.SIGN_STATUS_ERROR.getMsg(), signPdfInfo); 38 | case 3: 39 | return new VerifyResponse(com.resrun.enums.SignStatus.SIGN_STATUS_FIDDLE.getMsg(), signPdfInfo); 40 | default: 41 | return new VerifyResponse(com.resrun.enums.SignStatus.SIGN_STATUS_RIGHT.getMsg(), signPdfInfo); 42 | } 43 | 44 | 45 | }catch (Exception e){ 46 | signPdfInfo.setPdfName(fileName); 47 | signPdfInfo.setPdfSize(String.valueOf(bytes.length/1024)); 48 | 49 | signPdfInfo.setPdfSingResult(com.resrun.enums.SignStatus.SIGN_STATUS_NOSIGNATURE.getCode()); 50 | return new VerifyResponse(com.resrun.enums.SignStatus.SIGN_STATUS_NOSIGNATURE.getMsg(),signPdfInfo); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/service/verify/VerifySign.java: -------------------------------------------------------------------------------- 1 | package com.resrun.service.verify; 2 | 3 | 4 | import com.itextpdf.text.Rectangle; 5 | import com.itextpdf.text.pdf.*; 6 | import com.itextpdf.text.pdf.parser.ContentByteUtils; 7 | import com.itextpdf.text.pdf.parser.PdfContentStreamProcessor; 8 | import com.itextpdf.text.pdf.security.CertificateInfo; 9 | import com.itextpdf.text.pdf.security.PdfPKCS7; 10 | import com.resrun.enums.SignStatus; 11 | import com.resrun.service.pojo.SignPdfInfoVo; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.bouncycastle.asn1.ASN1InputStream; 14 | import org.bouncycastle.asn1.ASN1ObjectIdentifier; 15 | import org.bouncycastle.asn1.ASN1Sequence; 16 | import org.bouncycastle.asn1.x509.TBSCertificate; 17 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 18 | 19 | import javax.imageio.ImageIO; 20 | import javax.swing.*; 21 | import java.awt.*; 22 | import java.awt.image.BufferedImage; 23 | import java.io.*; 24 | import java.lang.reflect.Field; 25 | import java.security.GeneralSecurityException; 26 | import java.security.Principal; 27 | import java.security.Security; 28 | import java.security.cert.X509Certificate; 29 | import java.util.ArrayList; 30 | import java.util.Base64; 31 | import java.util.List; 32 | import java.util.Set; 33 | 34 | /** 35 | * @Description: 在线签名验签工具类 36 | * @Package: com.resrun.service.verify 37 | * @ClassName: VerifySign 38 | * @copyright 北京资源律动科技有限公司 39 | */ 40 | @Slf4j 41 | public class VerifySign { 42 | 43 | static { 44 | Security.addProvider(new BouncyCastleProvider()); 45 | } 46 | 47 | /** 48 | * 验证PDF证书信息是否有效 49 | * @throws IOException 50 | * @throws GeneralSecurityException 51 | */ 52 | public static Integer checkSignIsValid(String path) { 53 | Integer signStatus = SignStatus.SIGN_STATUS_RIGHT.getCode(); 54 | try{ 55 | 56 | FileInputStream fis = new FileInputStream(path); 57 | PdfReader reader = new PdfReader(fis); 58 | int numberOfPages = reader.getNumberOfPages(); 59 | 60 | PdfReader pdfReader = new PdfReader(path); 61 | AcroFields acroFields = pdfReader.getAcroFields(); 62 | List namesList = acroFields.getSignatureNames(); 63 | 64 | if(namesList!=null && namesList.size()<=0){ 65 | signStatus=SignStatus.SIGN_STATUS_NOSIGNATURE.getCode(); 66 | } 67 | 68 | for (String name : namesList) { 69 | PdfDictionary signatureDict = acroFields.getSignatureDictionary(name); 70 | //时间戳 71 | String timestrap = signatureDict.getAsString(PdfName.M).toString().replace("D:","").substring(0,12); 72 | 73 | PdfPKCS7 pdfPKCS7 = acroFields.verifySignature(name); 74 | 75 | X509Certificate x509Certificate = pdfPKCS7.getSigningCertificate(); 76 | Principal principal = x509Certificate.getIssuerDN(); 77 | //证书颁发机构 78 | String s = principal.toString().split("CN")[2].replace("=",""); 79 | //时间戳有效性 80 | boolean flag = pdfPKCS7.verifyTimestampImprint(); 81 | //签署人姓名 82 | String signerName = CertificateInfo.getSubjectFields(pdfPKCS7.getSigningCertificate()).getField("CN"); //证书信息 83 | //文档是否被修改 84 | boolean isChange = pdfPKCS7.verify(); 85 | if (isChange) { 86 | // log.info("有效签名"); 87 | signStatus=SignStatus.SIGN_STATUS_RIGHT.getCode(); 88 | } else { 89 | // log.info("无效签名"); 90 | signStatus=SignStatus.SIGN_STATUS_FIDDLE.getCode(); 91 | } 92 | // log.info(signerName + "\t时间戳是否有效:" +flag + "\t" + timestrap + "\t颁发机构:" +s+ "\t是否被篡改:"+isChange); 93 | } 94 | return signStatus; 95 | 96 | }catch (Exception e){ 97 | signStatus=SignStatus.SIGN_STATUS_ERROR.getCode(); 98 | } 99 | return signStatus; 100 | } 101 | 102 | 103 | 104 | /** 105 | * 获取PDF中数字签名信息 106 | * @param file pdf文件 107 | * @return 提取结果 108 | */ 109 | public static SignPdfInfoVo getSignFromPdf(byte [] file) throws IOException { 110 | SignPdfInfoVo pdfSignInfo = new SignPdfInfoVo(); 111 | // String fileBasePath = System.getProperty("user.dir") + "/resrun-paas-system/src/main/resources/upload/"; 112 | // File oldFile = new File(path); 113 | try { 114 | ByteArrayInputStream fis = new ByteArrayInputStream(file); 115 | PdfReader reader = new PdfReader(fis); 116 | 117 | // String fileName = path.substring(path.lastIndexOf("/") + 1, path.lastIndexOf(".")); 118 | // if (!fileBasePath.endsWith("/")) { 119 | // fileBasePath = fileBasePath + "/"; 120 | // } 121 | 122 | 123 | // pdfSignInfo.setPdfName(path.substring(path.lastIndexOf("/") + 1)); 124 | // pdfSignInfo.setPdfSize(String.valueOf(oldFile.length())); 125 | Field rsaDataField = PdfPKCS7.class.getDeclaredField("RSAdata"); 126 | rsaDataField.setAccessible(true); 127 | 128 | int numberOfPages = reader.getNumberOfPages(); 129 | 130 | int xrefSize = reader.getXrefSize(); 131 | 132 | 133 | // listener.setBasePath(fileBasePath); 134 | 135 | // 获取acro字段 136 | AcroFields fields = reader.getAcroFields(); 137 | Rectangle pageSize = reader.getPageSize(numberOfPages ); 138 | // pdf 作用域尺寸(全屏) 139 | // System.out.println("pdf rectangle: " + pageSize.getLeft() + "," + pageSize.getBottom() + "," + pageSize.getRight() + "," + pageSize.getTop()); 140 | // 获取签名名称 141 | ArrayList signatureNames = fields.getSignatureNames(); 142 | if(signatureNames!=null && signatureNames.size()<=0){ 143 | pdfSignInfo.setPdfSingResult(SignStatus.SIGN_STATUS_NOSIGNATURE.getCode()); 144 | } 145 | for (String name : signatureNames) { 146 | ExtImageRenderListener listener = new ExtImageRenderListener(); 147 | // 指定提供者 获取签名的pkcs7数据 148 | PdfPKCS7 pkcs7 = fields.verifySignature(name); 149 | X509Certificate certificate = pkcs7.getSigningCertificate(); 150 | SignPdfInfoVo.SignatureDetail info = new SignPdfInfoVo.SignatureDetail(); 151 | boolean isChange = pkcs7.verify(); 152 | if (isChange) { 153 | pdfSignInfo.setPdfSingResult(SignStatus.SIGN_STATUS_RIGHT.getCode()); 154 | } else { 155 | pdfSignInfo.setPdfSingResult(SignStatus.SIGN_STATUS_FIDDLE.getCode()); 156 | } 157 | 158 | String[] appearanceStates = fields.getAppearanceStates(name); 159 | // 表单域的位置 160 | List fieldPositions = fields.getFieldPositions(name); 161 | for (AcroFields.FieldPosition fieldPosition : fieldPositions) { 162 | Rectangle position = fieldPosition.position; 163 | 164 | info.pageNum = fieldPosition.page; 165 | info.llx = position.getLeft(); 166 | info.lly = position.getBottom(); 167 | info.urx = position.getRight(); 168 | info.ury = position.getTop(); 169 | } 170 | 171 | AcroFields.Item fieldItem = fields.getFieldItem(name); 172 | int size = fieldItem.size(); 173 | for (int i = 0; i < size; i++) { 174 | PdfDictionary value = fieldItem.getValue(i); 175 | Set keys = value.getKeys(); 176 | for (PdfName key : keys) { 177 | String keyName = PdfName.decodeName(new String(key.getBytes())); 178 | if ("Rect".equalsIgnoreCase(keyName)) { 179 | log.debug("签名域的坐标为:" + value.get(key)); 180 | } 181 | } 182 | PdfObject pdfObject = value.get(PdfName.AP); 183 | if (pdfObject != null) { 184 | PdfDictionary dictionary = (PdfDictionary) pdfObject; 185 | PdfIndirectReference ref = (PdfIndirectReference) dictionary.get(PdfName.N); 186 | int number = ref.getNumber(); 187 | 188 | // 根据ap 获取签章图片 189 | PdfObject pdfObjectRelease = reader.getPdfObject(number); 190 | if (pdfObjectRelease instanceof PdfStream) { 191 | PdfStream s = (PdfStream) pdfObjectRelease; 192 | PdfDictionary resources = s.getAsDict(PdfName.RESOURCES); 193 | listener.setI(number); 194 | try { 195 | 196 | PdfContentStreamProcessor processor = new PdfContentStreamProcessor(listener); 197 | processor.processContent(ContentByteUtils.getContentBytesFromContentObject(s), resources); 198 | if(listener.getSeal() != null && listener.getSeal().length>0){ 199 | byte [] seal = changeImgColor(listener.getSeal()); 200 | info.sealBase64 = Base64.getEncoder().encodeToString(seal); 201 | } 202 | } catch (Exception ignore) { 203 | ignore.printStackTrace(); 204 | pdfSignInfo.setPdfSingResult(SignStatus.SIGN_STATUS_ERROR.getCode()); 205 | } 206 | } 207 | } 208 | } 209 | // certificate.getSignature() 210 | // TBSCertificate tbsCertificate = new TBSCertificate(); 211 | // pkcs7.getSigningCertificate(); 212 | ByteArrayInputStream bIn = new ByteArrayInputStream(certificate.getTBSCertificate()); 213 | ASN1InputStream aIn = new ASN1InputStream(bIn); 214 | ASN1Sequence sequence = (ASN1Sequence) aIn.readObject(); 215 | TBSCertificate tbs = TBSCertificate.getInstance(sequence); 216 | ASN1ObjectIdentifier asn1ObjectIdentifierCN = new ASN1ObjectIdentifier("2.5.4.3"); 217 | ASN1ObjectIdentifier asn1ObjectIdentifierO = new ASN1ObjectIdentifier("2.5.4.10"); 218 | if(pkcs7.getTimeStampToken() == null){ 219 | info.location = "本地时间"; 220 | }else{ 221 | info.location = "可信时间认证机构"; 222 | } 223 | // 签名信息 224 | info.signName = name; 225 | info.signTime = pkcs7.getSignDate().getTime(); 226 | info.validStartTime = certificate.getNotBefore(); 227 | info.validEndTime = certificate.getNotAfter(); 228 | // info.certName = certificate.getSubjectDN().getName(); 229 | info.certName = tbs.getSubject().getRDNs(asn1ObjectIdentifierCN)[0].getFirst().getValue().toString(); 230 | // info.serialNumber = String.valueOf(certificate.getSerialNumber()); 231 | info.serialNumber = tbs.getSerialNumber().getValue().toString(16); 232 | Base64.Encoder encoder = Base64.getEncoder(); 233 | info.publicKey = encoder.encodeToString(certificate.getPublicKey().getEncoded()); 234 | info.pubKeyFormat = certificate.getPublicKey().getFormat(); 235 | info.sigAlgName = certificate.getSigAlgName(); 236 | // info.userDnName = certificate.getIssuerDN().getName(); 237 | info.userDnName = tbs.getIssuer().getRDNs(asn1ObjectIdentifierCN)[0].getFirst().getValue().toString(); 238 | info.reason = pkcs7.getReason(); 239 | // info.location = pkcs7.getLocation(); 240 | info.validate = pkcs7.verify(); 241 | Field digestAttrField = PdfPKCS7.class.getDeclaredField("digestAttr"); 242 | digestAttrField.setAccessible(true); 243 | byte[] digestAttr = (byte[]) digestAttrField.get(pkcs7); 244 | info.fileDigest = encoder.encodeToString(digestAttr); 245 | 246 | 247 | pdfSignInfo.getSignatureDetails().add(info); 248 | 249 | } 250 | } catch (Exception e) { 251 | pdfSignInfo.setPdfSingResult(SignStatus.SIGN_STATUS_ERROR.getCode()); 252 | log.error("取PDF中数字签名信息失败", e.getMessage()); 253 | } 254 | return pdfSignInfo; 255 | } 256 | 257 | 258 | 259 | /** 260 | * 将背景替换为透明 261 | * @return 262 | * @throws IOException the io exception 263 | */ 264 | public static void changeImgColor(String path) throws IOException { 265 | 266 | File file = new File(path); 267 | String fileName = file.getName(); 268 | BufferedImage bi = ImageIO.read(file); 269 | 270 | BufferedImage image = bi; 271 | //将原图片的二进制转化为ImageIcon 272 | ImageIcon imageIcon = new ImageIcon(image); 273 | int width = imageIcon.getIconWidth(); 274 | int height = imageIcon.getIconHeight(); 275 | 276 | //图片缓冲流 277 | BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); 278 | Graphics2D graphics2D = (Graphics2D) bufferedImage.getGraphics(); 279 | graphics2D.drawImage(imageIcon.getImage(), 0, 0, imageIcon.getImageObserver()); 280 | 281 | int alpha = 255; 282 | 283 | //这个背景底色的选择,我这里选择的是比较偏的位置,可以修改位置。背景色选择不知道有没有别的更优的方式(比如先过滤一遍获取颜色次数最多的,但是因为感觉做起来会比较复杂没去实现),如果有可以评论。 284 | int RGB=bufferedImage.getRGB(width-1, height-1); 285 | 286 | for(int i = bufferedImage.getMinX(); i < width; i++) { 287 | for(int j = bufferedImage.getMinY(); j < height; j++) { 288 | 289 | int rgb = bufferedImage.getRGB(i, j); 290 | 291 | int r = (rgb & 0xff0000) >>16; 292 | int g = (rgb & 0xff00) >> 8; 293 | int b = (rgb & 0xff); 294 | int R = (RGB & 0xff0000) >>16; 295 | int G = (RGB & 0xff00) >> 8; 296 | int B = (RGB & 0xff); 297 | //a为色差范围值,渐变色边缘处理,数值需要具体测试,50左右的效果比较可以 298 | int a = 45; 299 | if(Math.abs(R-r) < a && Math.abs(G-g) < a && Math.abs(B-b) < a ) { 300 | alpha = 0; 301 | } else { 302 | alpha = 255; 303 | } 304 | rgb = (alpha << 24)|(rgb & 0x00ffffff); 305 | bufferedImage.setRGB(i,j,rgb); 306 | } 307 | } 308 | 309 | String[] split = fileName.split("\\."); 310 | fileName = split[0]+"(已转换)."+split[1]; 311 | ImageIO.write(bufferedImage, "png", new File(path)); 312 | } 313 | 314 | /** 315 | * 将背景替换为透明 316 | * @return 317 | * @throws IOException the io exception 318 | */ 319 | public static byte[] changeImgColor(byte [] seal) throws IOException { 320 | 321 | ByteArrayInputStream bis = new ByteArrayInputStream(seal); 322 | // String fileName = file.getName(); 323 | BufferedImage bi = ImageIO.read(bis); 324 | 325 | BufferedImage image = bi; 326 | //将原图片的二进制转化为ImageIcon 327 | ImageIcon imageIcon = new ImageIcon(image); 328 | int width = imageIcon.getIconWidth(); 329 | int height = imageIcon.getIconHeight(); 330 | 331 | //图片缓冲流 332 | BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); 333 | Graphics2D graphics2D = (Graphics2D) bufferedImage.getGraphics(); 334 | graphics2D.drawImage(imageIcon.getImage(), 0, 0, imageIcon.getImageObserver()); 335 | 336 | int alpha = 255; 337 | 338 | //这个背景底色的选择,我这里选择的是比较偏的位置,可以修改位置。背景色选择不知道有没有别的更优的方式(比如先过滤一遍获取颜色次数最多的,但是因为感觉做起来会比较复杂没去实现),如果有可以评论。 339 | int RGB=bufferedImage.getRGB(width-1, height-1); 340 | 341 | for(int i = bufferedImage.getMinX(); i < width; i++) { 342 | for(int j = bufferedImage.getMinY(); j < height; j++) { 343 | 344 | int rgb = bufferedImage.getRGB(i, j); 345 | 346 | int r = (rgb & 0xff0000) >>16; 347 | int g = (rgb & 0xff00) >> 8; 348 | int b = (rgb & 0xff); 349 | int R = (RGB & 0xff0000) >>16; 350 | int G = (RGB & 0xff00) >> 8; 351 | int B = (RGB & 0xff); 352 | //a为色差范围值,渐变色边缘处理,数值需要具体测试,50左右的效果比较可以 353 | int a = 5; 354 | if(Math.abs(R-r) < a && Math.abs(G-g) < a && Math.abs(B-b) < a ) { 355 | alpha = 0; 356 | } else { 357 | alpha = 255; 358 | } 359 | rgb = (alpha << 24)|(rgb & 0x00ffffff); 360 | bufferedImage.setRGB(i,j,rgb); 361 | } 362 | } 363 | 364 | // String[] split = fileName.split("\\."); 365 | // fileName = split[0]+"(已转换)."+split[1]; 366 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 367 | ImageIO.write(bufferedImage, "png", bos); 368 | return bos.toByteArray(); 369 | } 370 | 371 | 372 | public static void main(String[] args) throws IOException { 373 | // checkSignIsValid("/Users/huaiyong/Desktop/B.pdf"); 374 | // File file = new File("F://download//电信电子发票202308142220 (1).pdf"); 375 | // File file = new File("F://download//版权登记申请代理委托合同.pdf"); 376 | File file = new File("F://download//签署文件-1678695004917124386-北京小轻新商贸中心.pdf"); 377 | 378 | FileInputStream fis = new FileInputStream(file); 379 | ByteArrayOutputStream bos = new ByteArrayOutputStream(1000); 380 | byte[] b = new byte[1000]; 381 | int n; 382 | while ((n = fis.read(b)) != -1) { 383 | bos.write(b, 0, n); 384 | } 385 | byte[] data = bos.toByteArray(); 386 | getSignFromPdf(data); 387 | fis.close(); 388 | // getSignFromPdf("B.pdf"); 389 | } 390 | 391 | } 392 | -------------------------------------------------------------------------------- /src/main/java/com/resrun/utils/Base64.java: -------------------------------------------------------------------------------- 1 | package com.resrun.utils; 2 | 3 | 4 | import java.io.ByteArrayOutputStream; 5 | 6 | /** 7 | * @Description: base64转换 8 | * @Package: com.resrun.utils 9 | * @ClassName: Base64 10 | * @copyright 北京资源律动科技有限公司 11 | */ 12 | public class Base64 { 13 | 14 | private static char[] base64EncodeChars = new char[] { 'A', 'B', 'C', 'D', 15 | 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 16 | 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 17 | 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 18 | 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', 19 | '4', '5', '6', '7', '8', '9', '+', '/' }; 20 | 21 | private static byte[] base64DecodeChars = new byte[] { -1, -1, -1, -1, -1, 22 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 23 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24 | -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 25 | 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 26 | 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, 27 | -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 28 | 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, 29 | -1, -1 }; 30 | 31 | private Base64(){ 32 | } 33 | 34 | public static String encode(byte[] data){ 35 | StringBuffer sb = new StringBuffer(); 36 | int len = data.length; 37 | int i = 0; 38 | int b1, b2, b3; 39 | 40 | while (i < len) { 41 | b1 = data[i++] & 0xff; 42 | if (i == len) { 43 | sb.append(base64EncodeChars[b1 >>> 2]); 44 | sb.append(base64EncodeChars[(b1 & 0x3) << 4]); 45 | sb.append("=="); 46 | break; 47 | } 48 | b2 = data[i++] & 0xff; 49 | if (i == len) { 50 | sb.append(base64EncodeChars[b1 >>> 2]); 51 | sb.append(base64EncodeChars[((b1 & 0x03) << 4) 52 | | ((b2 & 0xf0) >>> 4)]); 53 | sb.append(base64EncodeChars[(b2 & 0x0f) << 2]); 54 | sb.append("="); 55 | break; 56 | } 57 | b3 = data[i++] & 0xff; 58 | sb.append(base64EncodeChars[b1 >>> 2]); 59 | sb.append(base64EncodeChars[((b1 & 0x03) << 4) 60 | | ((b2 & 0xf0) >>> 4)]); 61 | sb.append(base64EncodeChars[((b2 & 0x0f) << 2) 62 | | ((b3 & 0xc0) >>> 6)]); 63 | sb.append(base64EncodeChars[b3 & 0x3f]); 64 | } 65 | return sb.toString(); 66 | } 67 | 68 | public static byte[] decode(String str){ 69 | byte[] data = str.getBytes(); 70 | int len = data.length; 71 | ByteArrayOutputStream buf = new ByteArrayOutputStream(len); 72 | int i = 0; 73 | int b1, b2, b3, b4; 74 | 75 | while (i < len) { 76 | 77 | /* b1 */ 78 | do { 79 | b1 = base64DecodeChars[data[i++]]; 80 | } while (i < len && b1 == -1); 81 | if (b1 == -1) { 82 | break; 83 | } 84 | 85 | /* b2 */ 86 | do { 87 | b2 = base64DecodeChars[data[i++]]; 88 | } while (i < len && b2 == -1); 89 | if (b2 == -1) { 90 | break; 91 | } 92 | buf.write(((b1 << 2) | ((b2 & 0x30) >>> 4))); 93 | 94 | /* b3 */ 95 | do { 96 | b3 = data[i++]; 97 | if (b3 == 61) { 98 | return buf.toByteArray(); 99 | } 100 | b3 = base64DecodeChars[b3]; 101 | } while (i < len && b3 == -1); 102 | if (b3 == -1) { 103 | break; 104 | } 105 | buf.write((((b2 & 0x0f) << 4) | ((b3 & 0x3c) >>> 2))); 106 | 107 | /* b4 */ 108 | do { 109 | b4 = data[i++]; 110 | if (b4 == 61) { 111 | return buf.toByteArray(); 112 | } 113 | b4 = base64DecodeChars[b4]; 114 | } while (i < len && b4 == -1); 115 | if (b4 == -1) { 116 | break; 117 | } 118 | buf.write((((b3 & 0x03) << 6) | b4)); 119 | } 120 | return buf.toByteArray(); 121 | } 122 | } -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | server: 2 | # 项目启动端口号 3 | port: 8666 4 | tomcat: 5 | # 上传文件限制 -1 无限制 6 | max-swallow-size: -1 7 | servlet: 8 | #路径前缀 9 | context-path: /openSign 10 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: dev -------------------------------------------------------------------------------- /src/main/resources/开源工具版说明.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenghuihan/open-sign/f1fa38ee6e9ce4dc467123c67264c6f88b1d4559/src/main/resources/开源工具版说明.pdf --------------------------------------------------------------------------------