Loading

Itext7实现签章工具

引入依赖

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext7-core</artifactId>
    <version>8.0.5</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcutil-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>bouncy-castle-adapter</artifactId>
    <version>9.1.0</version>
</dependency>

生成证书文件

# windows利用java环境执行该命令
keytool -genkey -alias RSAkey -keystore cert.p12 -storetype PKCS12 -keyalg RSA -storepass test@123 -validity 3560 -keysize 2048 -dname "CN=XXX公司, OU=XXX公司, O=XXX公司"

工具类实现

PDFSignUtil

/**
 * <p>
 * <B>Description: PDF数字签名工具</B>
 * </P>
 * Revision Trail: (Date/Author/Description)
 * 2025/3/26 Ryan Huang CREATE
 *
 * @author Ryan Huang
 * @version 1.0
 */
public class PDFSignUtil {

	static {
		Security.addProvider(new BouncyCastleProvider());
	}

	private static final Logger log = LoggerFactory.getLogger(PDFSignUtil.class);

	/**
	 * 证书类型
	 */
	private static final String PKCS12 = "PKCS12";

	/**
	 * 签章名称前缀
	 */
	private static final String PDF_SEAL_FILE_NAME_PRE = "SEAL";
	/**
	 * 数字签名前缀
	 */
	private static final String PDF_SIGN_FILE_NAME_PRE = "SIGN";
	/**
	 * 签名类型:数字签名
	 */
	private static final String SIGN_TYPE_DIGITAL = "DIGITAL_SIGN";
	/**
	 * 签名类型:印章
	 */
	private static final String SIGN_TYPE_SEAL = "SEAL_SIGN";

	/**
	 * 证书信息
	 */
	private static final String CERT_SUBJECT_DN = "CN=XXX公司,OU=XXX公司,O=XXX公司";

	/**
	 * PDF加盖图章(固定位置)
	 *
	 * @param password      	证书密码
	 * @param certPath 	        证书路径
	 * @param pdfPath           PDF路径
	 * @param sealImagePath     图章路径
	 * @param x                 图章X坐标,坐标轴的原点(0,0)位于该页面的左下角。X 轴就是左下角到右下角的那条直线
	 * @param y                 图章Y坐标,坐标轴的原点(0,0)位于该页面的左下角。Y 轴就是左下角到左上角的那条直线
	 * @param sealImageWidth    图章宽度
	 * @param sealImageHeight   图章高度
	 * @param pageNumArray     需要加盖图章的页码数组,为空则选择全部
	 *
	 * @return              加盖图章后的PDF字节数组
	 */
	public static byte[] sealWithImage(String password, String certPath, String pdfPath, String sealImagePath,
							  float x, float y, int sealImageWidth, int sealImageHeight, int[] pageNumArray) {
		try (InputStream certFis = new FileInputStream(certPath);
			 InputStream pdfFis = new FileInputStream(pdfPath);
			 InputStream stampFis = new FileInputStream(sealImagePath)) {
			 return sealWithImage(password, certFis, pdfFis, stampFis, x, y, sealImageWidth, sealImageHeight, pageNumArray);
		}catch (Exception e){
			throw new RuntimeException(e);
		}
	}

	/**
	 * PDF加盖图章(固定位置)
	 *
	 * @param password        证书密码
	 * @param certFis         证书输入流
	 * @param pdfFis          PDF输入流
	 * @param sealImageFis    图章输入流
	 * @param x               图章X坐标,坐标轴的原点(0,0)位于该页面的左下角。X 轴就是左下角到右下角的那条直线
	 * @param y               图章Y坐标,坐标轴的原点(0,0)位于该页面的左下角。Y 轴就是左下角到左上角的那条直线
	 * @param sealImageWidth  图章宽度
	 * @param sealImageHeight 图章高度
	 * @param pageNumArray    需要加盖图章的页码数组,为空则选择全部
	 *
	 * @return              加盖图章后的PDF字节数组
	 */
	public static byte[] sealWithImage(String password, InputStream certFis, InputStream pdfFis, InputStream sealImageFis,
									   float x, float y, int sealImageWidth, int sealImageHeight, int[] pageNumArray) {
		byte[] pdfIsByte = inputStreamToByteArray(pdfFis);
		pdfFis = new ByteArrayInputStream(pdfIsByte);
		byte[] sealImageByte = inputStreamToByteArray(sealImageFis);
		// 需要签章的页码数组
		int[] processedPageNums = getPageNumArray(pdfIsByte, pageNumArray);

		// 每页签章时的临时文件输出流
		ByteArrayOutputStream temAos = null;

		try (ByteArrayOutputStream resultAos = new ByteArrayOutputStream()){
			// 获取PKCS12证书信息
			KeyStore keyStore = KeyStore.getInstance(PKCS12, BouncyCastleProvider.PROVIDER_NAME);
			keyStore.load(certFis, password.toCharArray());
			String alias = keyStore.aliases().nextElement();
			PrivateKey key = (PrivateKey) keyStore.getKey(keyStore.aliases().nextElement(), password.toCharArray());
			Certificate[] chain = keyStore.getCertificateChain(alias);
			certFis.close();
			certFis = null;

			// 对需要签章的页码进行签章
			for (int pageNum : processedPageNums) {
				temAos = processSeal(pdfFis, x, y, sealImageWidth, sealImageHeight, pageNum, sealImageByte, key, chain);
				// 定义输入流为生成的输出流内容,已完成多次签章
				pdfFis = new ByteArrayInputStream(temAos.toByteArray());
				resultAos.reset();
				resultAos.write(temAos.toByteArray());

			}
			return resultAos.toByteArray();
		}catch (Exception e){
			throw new RuntimeException("PDF签章失败!", e);
		}finally {
			try {
				pdfFis.close();
				if (temAos != null) {
					temAos.close();
				}
				if (certFis != null) {
					certFis.close();
				}
			} catch (IOException e) {
				log.error("PDF签章资源关闭异常: ", e);
			}

		}
	}


	/**
	 * PDF加盖图章(文本相对位置)
	 *
	 * @param password       证书密码
	 * @param certPath       证书路径
	 * @param pdfPath        待加盖图章的PDF路径
	 * @param sealImagePath  图章路径
	 * @param targetText     目标文本
	 * @param offsetX        X偏移量
	 * @param offsetY        Y偏移量
	 * @param sealImageWidth 图章宽度
	 * @param sealImageHeight 图章高度
	 * @param pageNumArray    需要加盖图章的页码数组,为空则选择全部
	 *
	 * @return              加盖图章后的PDF字节数组
	 */

	public static byte[] sealWithImage(String password, String certPath, String pdfPath, String sealImagePath, String targetText,
									   float offsetX, float offsetY, int sealImageWidth, int sealImageHeight, int[] pageNumArray) {
		try (InputStream certFis = new FileInputStream(certPath);
			 InputStream pdfFis = new FileInputStream(pdfPath);
			 InputStream stampFis = new FileInputStream(sealImagePath)) {
			return sealWithImage(password, certFis, pdfFis, stampFis, targetText, offsetX, offsetY, sealImageWidth, sealImageHeight, pageNumArray);
		}catch (Exception e){
			throw new RuntimeException(e);
		}
	}


	/**
	 * PDF加盖图章(文本相对位置)
	 *
	 * @param password      证书密码
	 * @param certFis       证书输入流
	 * @param pdfFis        待加盖图章的PDF输入流
	 * @param sealImageFis  图章输入流
	 * @param targetText     目标文本
	 * @param offsetX        X轴偏移量,中心点为匹配文本第一个字符最左下角位置
	 * @param offsetY        Y偏移量,中心点为匹配文本第一个字符最左下角位置
	 * @param sealImageWidth 图章宽度
	 * @param sealImageHeight 图章高度
	 * @param pageNumArray    需要加盖图章的页码数组,为空则选择全部
	 *
	 * @return              加盖图章后的PDF字节数组
	 */

	public static byte[] sealWithImage(String password, InputStream certFis, InputStream pdfFis, InputStream sealImageFis, String targetText,
									   float offsetX, float offsetY, int sealImageWidth, int sealImageHeight, int[] pageNumArray) {
		// 获取需要加盖图章的页码数组
		byte[] pdfIsByte = inputStreamToByteArray(pdfFis);
		byte[] sealImageByte = inputStreamToByteArray(sealImageFis);
		pdfFis = new ByteArrayInputStream(pdfIsByte);
		int[] processedPageNums = getPageNumArray(pdfIsByte, pageNumArray);

		// 每页签章时的临时文件输出流
		ByteArrayOutputStream temAos = null;

		try (ByteArrayOutputStream resultAos = new ByteArrayOutputStream()) {
			// 获取目标文本位置
			Map<Integer, List<Rectangle>> textPositions = findTextPositions(new ByteArrayInputStream(pdfIsByte), targetText, processedPageNums);
			if(textPositions.isEmpty()) return pdfIsByte;

			// 获取PKCS12证书信息
			KeyStore keyStore = KeyStore.getInstance(PKCS12, BouncyCastleProvider.PROVIDER_NAME);
			keyStore.load(certFis, password.toCharArray());
			String alias = keyStore.aliases().nextElement();
			PrivateKey key = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
			Certificate[] chain = keyStore.getCertificateChain(alias);
			certFis.close();
			certFis = null;

			// 遍历找到的文本位置
			for (Map.Entry<Integer, List<Rectangle>> entry : textPositions.entrySet()) {
				int pageNum = entry.getKey();
				for (Rectangle textRect : entry.getValue()) {
					// 计算签章位置
					float x = textRect.getX() + offsetX;
					float y = textRect.getY() + offsetY;
					// 处理签章
					temAos = processSeal(pdfFis, x, y, sealImageWidth, sealImageHeight, pageNum, sealImageByte, key, chain);
					pdfFis = new ByteArrayInputStream(temAos.toByteArray());
					resultAos.reset();
					resultAos.write(temAos.toByteArray());
				}
			}
			return resultAos.toByteArray();
		} catch (Exception e) {
			throw new ServiceException("PDF文字相对位置签章失败: " + e.getMessage());
		}finally {
			try {
				if (certFis != null) {
					certFis.close();
				}
				pdfFis.close();
			} catch (IOException e) {
				log.error("PDF签章资源关闭异常: ", e);
			}

		}
	}


	/**
	 * pdf数字签名
	 *
	 * @param password      证书密码
	 * @param keyStorePath  证书路径
	 * @param pdfPath    PDF路径
	 *
	 * @return              数字签名后的PDF字节数组
	 */
	public static byte[] digitalSign(String password, String keyStorePath, String pdfPath) {
		try(FileInputStream fileInputStream = new FileInputStream(pdfPath);
			FileInputStream certFis = new FileInputStream(keyStorePath)) {
			return digitalSign(password, certFis, fileInputStream);
		}catch (IOException e){
			throw new ServiceException("PDF文件异常: " + e.getMessage());
		}
	}


	/**
	 * pdf数字签名
	 *
	 * @param password      证书密码
	 * @param certFis  证书路径
	 * @param pdfFis    数字签名PDF输入流
	 *
	 * @return              数字签名后的PDF字节数组
	 */
	public static byte[] digitalSign(String password, InputStream certFis, InputStream pdfFis) {
		// 获取PDF页码后输入流会被标记到结尾,所以需通过字节重新创建输入流
		byte[] pdfIsByte = inputStreamToByteArray(pdfFis);
		int[] pageNumArray = getDigitalPageNumArray(new ByteArrayInputStream(pdfIsByte));
		if(pageNumArray.length == 0) return pdfIsByte;
		pdfFis = new ByteArrayInputStream(pdfIsByte);

		// 结果文件输出流
		ByteArrayOutputStream resultAos = new ByteArrayOutputStream();
		// 每页签章时的临时文件输出流
		ByteArrayOutputStream temAos = null;

		try {
			// 获取PKCS12证书信息
			KeyStore keyStore = KeyStore.getInstance(PKCS12, BouncyCastleProvider.PROVIDER_NAME);
			keyStore.load(certFis, password.toCharArray());
			String alias = keyStore.aliases().nextElement();
			PrivateKey key = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
			Certificate[] chain = keyStore.getCertificateChain(alias);
			certFis.close();
			certFis = null;

			// 对需要签章的页码进行签章
			for (int pageNum : pageNumArray) {
				temAos = processDigitalSign(pdfFis, pageNum, key, chain);
				// 定义输入流为生成的输出流内容,已完成多次签章
				pdfFis = new ByteArrayInputStream(temAos.toByteArray());
				resultAos = temAos;

			}

			return resultAos.toByteArray();
		}catch (Exception e){
			throw new RuntimeException("PDF数字签名失败!", e);
		}finally {
			try {
				if (temAos != null) {
					temAos.close();
				}
				if (certFis != null) {
					certFis.close();
				}
				resultAos.close();
				pdfFis.close();
			} catch (IOException e) {
				log.error("PDF数字签名资源关闭异常: ", e);
			}

		}
	}


	/**
	 * 验证PDF数字签名
	 * 签发规则:
	 * 	1. 每个页面需有一个数字签名
	 * 	2. 数字签名名称必须以 SIGN 开头, 且签章名称需要需与页码一致,如签章名称为 SIGN1, 签章页码为1
	 * 	3. 数字签名的证书信息必须一致
	 * 	4. 签章原因字段 Reason -> 解密后的字符串并序列化后为Map {"curPage": 签章页码, "signType": "DIGITAL_SIGN"}
	 *
	 * @param pdfPath PDF文件路径
	 */
	public static boolean verifyDigitalSignature(String pdfPath) {
		try(FileInputStream pdfFis = new FileInputStream(pdfPath)){
			return verifyDigitalSignature(pdfFis);
		}catch (IOException e){
			throw new RuntimeException(e);
		}
	}

	/**
	 * 验证PDF数字签名
	 * 签发规则:
	 * 	1. 每个页面需有一个数字签名
	 * 	2. 数字签名名称必须以 SIGN 开头, 且签章名称需要需与页码一致,如签章名称为 SIGN1, 签章页码为1
	 * 	3. 数字签名的证书信息必须一致
	 * 	4. 签章原因字段 Reason -> 解密后的字符串并序列化后为Map {"curPage": 签章页码, "signType": "DIGITAL_SIGN"}
	 *
	 * @param pdfInputStream PDF文件输入流
	 */
	public static boolean verifyDigitalSignature(InputStream pdfInputStream) {
		try (PdfReader pdfReader = new PdfReader(pdfInputStream);
			 PdfDocument pdfDoc = new PdfDocument(pdfReader);){
			SignatureUtil signUtil = new SignatureUtil(pdfDoc);

			List<String> names = signUtil.getSignatureNames();
			if (names.isEmpty()) {
				log.info("PDF未签名");
				return false;
			};

			// 验证签名有效性
			for (String name : names) {
				PdfPKCS7 pkcs7 = signUtil.readSignatureData(name);
				if (!pkcs7.verifySignatureIntegrityAndAuthenticity()) {
					log.info("PDF签名有效性验证失败");
					return false;
				}
			}

			// 验证PDF是否为本公司签发
			long signNumber = names.stream()
					.filter(name -> name.contains(PDF_SIGN_FILE_NAME_PRE))
					.count();
			if (signNumber != pdfDoc.getNumberOfPages()) {
				log.info("数字签名数与页码数不一致");
				return false;
			}

			for (String name : names) {
				if (!name.contains(PDF_SIGN_FILE_NAME_PRE)) continue;
				PdfPKCS7 pkcs7 = signUtil.readSignatureData(name);
				Certificate[] chain = pkcs7.getSignCertificateChain();

				//验证数字签名证书
				if (chain != null && chain.length > 0) {
					X509Certificate cert = (X509Certificate) chain[0];
					String subjectDN = cert.getSubjectX500Principal().getName();
					if (!subjectDN.trim().equals(CERT_SUBJECT_DN)) {
						log.info("数字签名证书不匹配,当前数字签名证书:{}", subjectDN);
						return false;
					}
				}

				// 验证数字签名原因
				int pageNum = Integer.parseInt(name.substring(PDF_SIGN_FILE_NAME_PRE.length()));
				String reason = pkcs7.getReason();
				String decrypt = RSAUtil.decrypt(reason);
				HashMap<String, Object> reasonMap =  JsonUtil.parse(decrypt, new TypeReference<HashMap<String, Object>>() {});
				if (!(reasonMap.get("curPage") != null
						&& pageNum == Integer.parseInt(reasonMap.get("curPage").toString())
						&& SIGN_TYPE_DIGITAL.equals(reasonMap.get("signType")))) {
					log.info("数字签名原因不匹配!");
					return false;
				}
			}
		} catch (IOException e) {
			throw new ServiceException("PDF解析异常:" + e);
		} catch (GeneralSecurityException e) {
			log.error("PDF签名验证有效性失败:{}", e.getMessage());
		} catch (Exception e) {
			log.error("PDF签名验证失败:{}", e.getMessage());
		}finally {
			if(pdfInputStream != null){
				try {
					pdfInputStream.close();
				} catch (IOException e) {
					log.error("PDF签名验证资源关闭异常:{}", e.getMessage());
				}
			}
		}

		return true;
	}


	/**
	 * 查找指定文本在PDF中的位置
	 *
	 * @param pdfInputStream PDF文件输入流
	 * @param targetText     目标文本
	 * @param pageNumbers    需要查找的页码
	 * @return Map<Integer, List < Rectangle>>
	 */
	public static Map<Integer, List<Rectangle>> findTextPositions(InputStream pdfInputStream, String targetText, int[] pageNumbers) {
		Map<Integer, List<Rectangle>> positions = new HashMap<>();

		try (PdfReader pdfReader = new PdfReader(pdfInputStream);
			 PdfDocument pdfDoc = new PdfDocument(pdfReader)) {

			for (int pageNumber : pageNumbers) {
				// 使用自定义文本定位策略
				TextPositionStrategy strategy = new TextPositionStrategy(targetText);
				PdfCanvasProcessor processor = new PdfCanvasProcessor(strategy);
				processor.processPageContent(pdfDoc.getPage(pageNumber));

				List<Rectangle> found = strategy.getFoundPositions();
				if (!found.isEmpty()) {
					positions.put(pageNumber, found);
				}

			}
			return positions;
		} catch (Exception e) {
			throw new ServiceException(targetText + "文本查找失败:" + e.getMessage());
		}
	}

	/**
	 * 获取签名页码数组
	 *
	 * @param pdfInputStream    待签名PDF输入流
	 * @param pageNumArray      需要签名页码数组,为空,则代表都需签名
	 * @return int[]
	 */
	public static int[] getPageNumArray(InputStream pdfInputStream, int[] pageNumArray) {
		return getPageNumArray(inputStreamToByteArray(pdfInputStream), pageNumArray);
	}

	/**
	 * 获取签名页码数组
	 *
	 * @param pdfByte       待签名PDF字节数组
	 * @param pageNumArray  需要签名页码数组,为空,则代表都需签名
	 * @return int[]
	 */
	public static int[] getPageNumArray(byte[] pdfByte, int[] pageNumArray) {
		int totalPage = 0;
		//去重
		pageNumArray = Arrays.stream(pageNumArray).distinct().toArray();

		try (PdfReader pdfReader = new PdfReader(new ByteArrayInputStream(pdfByte));
			 PdfDocument pdfDoc = new PdfDocument(pdfReader)) {
			totalPage = pdfDoc.getNumberOfPages();
		}catch (Exception e){
			throw new ServiceException("获取PDF总页数失败: " + e.getMessage());
		}

		// 验证页码合法性
		if (pageNumArray != null) {
			for (int pageNum : pageNumArray) {
				if (pageNum < 1 || pageNum > totalPage) {
					throw new IllegalArgumentException("无效的页码: " + pageNum);
				}
			}
		}

		// 是否全部签名
		if (pageNumArray == null || pageNumArray.length == 0) {
			pageNumArray = new int[totalPage];
			for (int i = 0; i < totalPage; i++) {
				pageNumArray[i] = i + 1;
			}
		}

		return pageNumArray;
	}

	/**
	 * 获取需要数字签名的页码数组
	 * 		1. pageNumArray为空代表全部需要数字签名
	 * 	    2. 已经进行数字签名的不再进行数字签名
	 *
	 * @param pdfInputStream    待签名PDF输入流
	 * @return int[]
	 */
	public static int[] getDigitalPageNumArray(InputStream pdfInputStream) {
		try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfInputStream))) {
			SignatureUtil signUtil = new SignatureUtil(pdfDoc);
			List<String> names = signUtil.getSignatureNames();

			// 获取所有需要签名的页码(1~总页数)
			int totalPages = pdfDoc.getNumberOfPages();
			Set<Integer> unsignedPages = new HashSet<>();
			for (int i = 1; i <= totalPages; i++) {
				unsignedPages.add(i);
			}

			// 排除已签名页码
			for (String name : names) {
				if (name.startsWith(PDF_SIGN_FILE_NAME_PRE)) {
					try {
						int signedPage = Integer.parseInt(name.substring(PDF_SIGN_FILE_NAME_PRE.length()));
						unsignedPages.remove(signedPage);
					} catch (NumberFormatException e) {
						log.warn("无效的签名名称格式: {}", name);
					}
				}
			}

			// 转换为数组返回
			return unsignedPages.stream()
					.mapToInt(Integer::intValue)
					.toArray();
		} catch (Exception e) {
			throw new RuntimeException("获取需要数字签名的PDF页码失败! ",  e);
		}
	}

	/**
	 * 获取PDF总页数
	 *
	 * @param pdfInputStream PDF文件输入流
	 * @return int 总页数
	 */
	public static int getPageTotal(InputStream pdfInputStream) {
		try (PdfReader pdfReader = new PdfReader(pdfInputStream);
			 PdfDocument pdfDoc = new PdfDocument(pdfReader)) {
			return pdfDoc.getNumberOfPages();
		}catch (Exception e){
			throw new ServiceException("获取PDF总页数失败: " + e);
		}
	}

	/**
	 * 处理签章内容
	 *
	 * @param pdfFis PDF输入流
	 * @param x 签章X坐标
	 * @param y 图章Y坐标
	 * @param sealImageWidth 图章宽度
	 * @param sealImageHeight 图章高度
	 * @param pageNum 签章页码
	 * @param sealImageByte 图章字节
	 * @param key 私钥
	 * @param chain 证书链
	 *
	 * @return 签章完的输出流
	 */

	private static ByteArrayOutputStream processSeal(InputStream pdfFis, float x, float y, int sealImageWidth, int sealImageHeight,
													 int pageNum, byte[] sealImageByte, PrivateKey key, Certificate[] chain) {
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		try(PdfReader reader = new PdfReader(pdfFis)) {
			PdfSigner signer = new PdfSigner(reader, outputStream, new StampingProperties().useAppendMode());
			// 配置签章外观
			signer.setPageRect(new Rectangle(x, y, sealImageWidth, sealImageHeight));
			signer.setPageNumber(pageNum);
			signer.getSignatureAppearance()
					.setSignatureGraphic(ImageDataFactory.create(sealImageByte))
					.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
			// 设置签章算法
			IExternalDigest digest = new BouncyCastleDigest();
			IExternalSignature signature = new PrivateKeySignature(key, DigestAlgorithms.SHA256, BouncyCastleProvider.PROVIDER_NAME);
			// 执行签章
			signer.signDetached(digest, signature, chain,
					null, null, null, 0, PdfSigner.CryptoStandard.CADES);
		} catch (Exception e) {
			throw new ServiceException("PDF签章失败: " + e.getMessage());
		}
		return outputStream;
	}

	/**
	 * 处理数字签章
	 *
	 * @param pdfFis pdf输入流
	 * @param pageNum 页码
	 * @param key 私钥
	 * @param chain 证书链
	 * @return 签章完的输出流
	 */
	private static ByteArrayOutputStream processDigitalSign(InputStream pdfFis, int pageNum,  PrivateKey key, Certificate[] chain) {
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		try(PdfReader reader = new PdfReader(pdfFis);) {
			PdfSigner signer = new PdfSigner(reader, outputStream, new StampingProperties().useAppendMode());
			signer.setPageNumber(pageNum);
			// 设置每个签章的名称和签章验证内容
			signer.setFieldName(PDF_SIGN_FILE_NAME_PRE + pageNum);
			signer.setReason(getEncryptReason(pageNum, SIGN_TYPE_DIGITAL));
			// 设置签章算法
			IExternalDigest digest = new BouncyCastleDigest();
			IExternalSignature signature = new PrivateKeySignature(key, DigestAlgorithms.SHA256, BouncyCastleProvider.PROVIDER_NAME);
			// 执行签章
			signer.signDetached(digest, signature, chain,
					null, null, null, 0, PdfSigner.CryptoStandard.CADES);
		} catch (Exception e) {
			throw new ServiceException("PDF数字签章失败: " + e.getMessage());
		}
		return outputStream;
	}

	/**
	 * 获取加密原因
	 *
	 * @param pageNum 页码
	 * @param signType NUMBER_SIGN:数字
	 * @return 加密后的字符串
	 * @throws Exception 异常
	 */
	private static String getEncryptReason(int pageNum, String signType) throws Exception {
		HashMap<String, Object> reasonMap = new HashMap<>();
		// 当前页
		reasonMap.put("curPage", pageNum);
		// 签名类型 认证
		reasonMap.put("signType", signType);
		String reasonJson = JsonUtil.toJson(reasonMap);
		return RSAUtil.encrypt(reasonJson);
	}

	/**
	 * InputStream 转 ByteArray
	 *
	 * @param inputStream 输入流
	 * @return byte[]
	 */
	private static byte[] inputStreamToByteArray(InputStream inputStream) {
		try (BufferedInputStream bis = new BufferedInputStream(inputStream);
			 ByteArrayOutputStream baos = new ByteArrayOutputStream()) {

			byte[] buffer = new byte[8192];
			int bytesRead;
			while ((bytesRead = bis.read(buffer)) != -1) {
				baos.write(buffer, 0, bytesRead);
			}
			return baos.toByteArray();
		} catch (IOException e) {
			throw new RuntimeException("流转换失败", e);
		}
	}
}

文本定位策略实现

/**
 * <P>
 * <B>Description: IText7 文本定位策略</B>
 * </P>
 * Revision Trail: (Date/Author/Description)
 *                 2025/3/28 Ryan Huang CREATE
 * @author Ryan Huang
 * @version 1.0
 */
public class TextPositionStrategy extends LocationTextExtractionStrategy {

	/**
	 * 目标文本
	 */
	private final String targetText;

	/**
	 * 目标文本位置
	 */
	private final List<Rectangle> positions = new ArrayList<>();

	/**
	 * 记录字符及对应坐标
	 */
	private final List<Pair<Character, Rectangle>> characterList = new ArrayList<>();

	public TextPositionStrategy(String targetText) {
		this.targetText = targetText.toUpperCase(); // 根据需求调整大小写策略
	}

	@Override
	public void eventOccurred(IEventData data, EventType type) {
		super.eventOccurred(data, type);
		if (type != EventType.RENDER_TEXT) return;

		TextRenderInfo renderInfo = (TextRenderInfo) data;
		for (TextRenderInfo charInfo : renderInfo.getCharacterRenderInfos()) {
			String charStr = charInfo.getText();
			if (charStr.isEmpty()) continue;

			// 记录字符及对应坐标
			Rectangle rect = charInfo.getBaseline().getBoundingRectangle();
			Vector startPoint = charInfo.getBaseline().getStartPoint();
			characterList.add(new Pair<>(
				charStr.charAt(0),
				new Rectangle(
					startPoint.get(Vector.I1),
					startPoint.get(Vector.I2),
					rect.getWidth(),
					rect.getHeight()
				)
			));
		}
	}

	public List<Rectangle> getFoundPositions() {
		// 构建完整文本
		StringBuilder fullText = new StringBuilder();
		for (Pair<Character, Rectangle> pair : characterList) {
			fullText.append(pair.getKey());
		}

		// 查找所有匹配位置
		int index = 0;
		while ((index = fullText.indexOf(targetText, index)) != -1) {
			int endPos = index + targetText.length();
			if (endPos > characterList.size()) break;

			// 合并目标区域的坐标
			List<Rectangle> targetRects = characterList.subList(index, endPos)
				.stream()
				.map(Pair::getValue)
				.collect(Collectors.toList());

			positions.add(mergeRectangles(targetRects));
			index = endPos; // 继续查找后续匹配
		}
		return positions;
	}

	// 合并矩形区域
	private Rectangle mergeRectangles(List<Rectangle> rects) {
		float minX = Float.MAX_VALUE;
		float minY = Float.MAX_VALUE;
		float maxX = Float.MIN_VALUE;
		float maxY = Float.MIN_VALUE;

		for (Rectangle r : rects) {
			minX = Math.min(minX, r.getX());
			minY = Math.min(minY, r.getY());
			maxX = Math.max(maxX, r.getX() + r.getWidth());
			maxY = Math.max(maxY, r.getY() + r.getHeight());
		}

		return new Rectangle(
			minX,
			minY,
			maxX - minX,
			maxY - minY
		);
	}
}

加密解密工具实现

/**
 * <p>
 * <B>Description: RSA加密工具</B>
 * </P>
 * Revision Trail: (Date/Author/Description)
 * 2025/3/28 Ryan Huang CREATE
 *
 * @author Ryan Huang
 * @version 1.0
 */
public class RSAUtil {

	/**
	 * 加密算法
	 */
	private static final String ALGORITHM = "RSA";

	/**
	 * 密钥长度
	 */
	private static final int KEY_SIZE = 2048;

	/**
	 * 公钥
	 */
	private static final String PUBLIC_KEY = "";

	/**
	 * 私钥
	 */
	private static final String PRIVATE_KEY = "";

	/**
	 * 生成密钥对
	 *
	 * @return 结果
	 * @throws NoSuchAlgorithmException 异常
	 */
	public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
		keyPairGenerator.initialize(KEY_SIZE);
		return keyPairGenerator.generateKeyPair();
	}

	/**
	 * 公钥加密
	 *
	 * @param plainText 明文
	 * @param publicKey 公钥
	 *
	 * @return 加密后的字符串
	 * @throws Exception 异常
	 */
	public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
		Cipher cipher = Cipher.getInstance(ALGORITHM);
		cipher.init(Cipher.ENCRYPT_MODE, publicKey);
		byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
		return Base64.getEncoder().encodeToString(encryptedBytes);
	}

	/**
	 * 公钥加密
	 *
	 * @param plainText 明文
	 *
	 * @return 加密后的字符串
	 * @throws Exception 异常
	 */
	public static String encrypt(String plainText) throws Exception {
		Cipher cipher = Cipher.getInstance(ALGORITHM);
		Key publicKey = getPublicKeyFromString(PUBLIC_KEY);
		cipher.init(Cipher.ENCRYPT_MODE, publicKey);
		byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
		return Base64.getEncoder().encodeToString(encryptedBytes);
	}

	/**
	 * 私钥解密
	 *
	 * @param encryptedText 密文
	 * @param privateKey    私钥
	 *
	 * @return 解密后的字符串
	 * @throws Exception 异常
	 */
	public static String decrypt(String encryptedText, PrivateKey privateKey) throws Exception {
		Cipher cipher = Cipher.getInstance(ALGORITHM);
		cipher.init(Cipher.DECRYPT_MODE, privateKey);
		byte[] decodedBytes = Base64.getDecoder().decode(encryptedText);
		byte[] decryptedBytes = cipher.doFinal(decodedBytes);
		return new String(decryptedBytes);
	}

	/**
	 * 私钥解密
	 *
	 * @param encryptedText 密文
	 *
	 * @return 解密后的字符串
	 * @throws Exception 异常
	 */
	public static String decrypt(String encryptedText) throws Exception {
		Cipher cipher = Cipher.getInstance(ALGORITHM);
		Key privateKey = getPrivateKeyFromString(PRIVATE_KEY);
		cipher.init(Cipher.DECRYPT_MODE, privateKey);
		byte[] decodedBytes = Base64.getDecoder().decode(encryptedText);
		byte[] decryptedBytes = cipher.doFinal(decodedBytes);
		return new String(decryptedBytes);
	}

	/**
	 * 将公钥转换为字符串
	 *
	 * @param publicKey 公钥
	 * @return 字符串
	 */
	public static String getPublicKeyString(PublicKey publicKey) {
		return Base64.getEncoder().encodeToString(publicKey.getEncoded());
	}

	/**
	 * 从字符串中获取私钥
	 *
	 * @param privateKey 私钥
	 * @return 私钥字符串
	 */
	public static String getPrivateKeyString(PrivateKey privateKey) {
		return Base64.getEncoder().encodeToString(privateKey.getEncoded());
	}


	/**
	 * 从字符串中获取公钥
	 *
	 * @param publicKeyString 公钥
	 * @return 公钥字符串
	 * @throws Exception 异常
	 */
	public static PublicKey getPublicKeyFromString(String publicKeyString) throws Exception {
		byte[] keyBytes = Base64.getDecoder().decode(publicKeyString);
		X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
		KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
		return keyFactory.generatePublic(spec);
	}

	/**
	 * 从字符串中获取私钥
	 *
	 * @param privateKeyString 私钥
	 * @return 私钥字符串
	 * @throws Exception 异常
	 */
	public static PrivateKey getPrivateKeyFromString(String privateKeyString) throws Exception {
		byte[] keyBytes = Base64.getDecoder().decode(privateKeyString);
		PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
		KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
		return keyFactory.generatePrivate(spec);
	}

}

posted @ 2025-03-31 09:48  IamHzc  阅读(124)  评论(0)    收藏  举报