☰
PGP加密验签Java Demo:混合加密与接口报文安全实践
2026/10/21 6:13:00 网站建设 项目流程

简介:这是一套面向Java开发者的PGP加密验签与HTTPS安全通信完整示例包,特别适合在金融、政务等需要报文加密与双向认证的场景下学习参考。资源拆分为两个子项目:第一个覆盖PGP公钥加密、私钥解密、数字签名与验签全流程;第二个演示加密数据通过HTTP client发送,同时结合SSL/HTTPS携带CA证书,方便对接外部安全接口。压缩包共42个文件、约11.75MB,结构清晰,包含8个Java源码、9个编译后class、13个Bouncy Castle及HTTP工具依赖jar包,另含密钥key文件、asc公钥文件和Eclipse工程配置,可直接导入工程运行验证。内容还涉及Bouncy Castle相关API的调用方式、密钥对生成与加载要点,以及POST提交时携带证书的实现思路,可帮助减少PGP对接中的常见坑点。目前已有1027人学习下载,适合需要快速落地PGP与HTTPS双向认证的Java工程师参考;若需带CA证书发送的补充代码,可按资源页说明联系作者获取。

1. PGP 加密验签 Java Demo:它解决的是接口报文"密封+防抵赖"

对接外部接口的人大概率碰到过这种场景:对方扔给你一个 PGP 公钥文件,加一句"报文用 PGP 加密,再带上你们的私钥签名",然后等你把数据来回调通。JAVA-PGP 加密验签完整 Demo 就是这套流程的现成实现——从密钥生成、加密解密、签名验签,到配合 Java HTTP Client 和 SSL/HTTPS 做完整链路,试样齐全。适合正在做系统集成的后端工程师,也适合想把 PGP 交互从命令行 GnuPG 挪到 Java 进程内的人。下面按"原理 → 代码 → 传输 → 踩坑"拆一遍,新手能照着搭,老手可以直接对参数和边界。

2. PGP 原理与库选型:混合加密、Bouncy Castle 与密钥对分工

2.1 混合加密:会话密钥加密正文,公钥只保护会话密钥

很多人以为 PGP 就是"用公钥加密",这是最容易误会的点。RSA 非对称加密虽然直观,但性能和长度受限,直接加密几百 KB 的接口报文不现实。PGP 实际采用的是混合加密(Hybrid Cryptosystem):随机生成一个会话密钥(Session Key,AES 128/256),用它对正文做对称加密;再用接收方公钥对这个会话密钥做非对称加密。接收方拿到密文后,先用自己的私钥解出会话密钥,再用会话密钥解密正文。

这个设计有两个好处:公钥加密的只是 16/32 字节的会话密钥,开销可以忽略;正文走 AES,加解密速度快,不受 RSA 长度限制。在 HTTP 接口这种短报文场景里,通常几百毫秒内能完成全流程。签名是另一条独立链路:发送方对原文用私钥签名,接收方用发送方公钥验签。加密保护的是"别人看不了",签名保护的是"发送方赖不掉"——如果一个 Demo 里不把这两条链分开理解,后面调试加解密时很容易把密钥角色搞混。

2.2 选型对比:Bouncy Castle 与 GnuPG 命令行的边界

Java 世界里做 PGP 没有标准库,实际选择基本就两个:Bouncy Castle(BC)和调外部 GnuPG 命令。这份 Demo 走的是 Bouncy Castle 路线,原因很直接:进程内完成加解密,不依赖操作系统里的 gpg 可执行文件,部署环境更干净;加密、签名、密钥解析全是 Java API,方便与 Spring Boot、HTTP Client 等代码集成;GnuPG 的 exit code 和 stderr 解析是很多项目的隐患,BC 抛出的是可读异常栈。

GnuPG 命令行并非完全无用:生成密钥对和做联调对照试验时,我反而推荐命令行,因为 gpg 命令自检严格、兼容性好,生成出来的密钥可以直接给 Java 侧用。所以合理分工是:GnuPG 生成密钥、BC 做运行时加解密。这份 Demo 里两条路径都覆盖了,理解成"密钥生产用 gpg、业务跑批用 BC"最合适。

2.3 Demo 里的密钥体系:公钥加密、私钥解密、私钥签名、公钥验签

一个完整的 PGP 交互涉及两个参与方,每方一套密钥对(实际是主钥 + 加密子钥)。接 Demo 时先把角色对应清楚:

参与方持有密钥职责
发送方接收方公钥 + 自己私钥用接收方公钥加密会话密钥;用自己的私钥对原文签名
接收方自己私钥 + 发送方公钥用自己的私钥解出会话密钥;用发送方公钥验签

主钥和加密子钥的区别值得单独说:GnuPG 生成密钥时默认会带一个独立的加密子钥,主钥用来签名、子钥用来加密。Java 侧解析密钥环时,如果直接把主钥当加密钥用,遇到"这个 key 不可加密"的报错是常事。选公钥用isEncryptionKey()过滤,选私钥按 Key ID 精确匹配,这是后面避坑章节的核心。

代码里常见的密钥生成方式如下(联调阶段可免口令,生产务必加 passphrase):

# 生成接收方密钥对,%no-protection 表示私钥不设置口令,仅限本地联调 gpg --batch --generate-key <<EOF %no-protection Key-Type: RSA Key-Length: 2048 Subkey-Type: RSA Subkey-Length: 2048 Name-Real: DemoRecv Name-Email: recv@demo.local Expire-Date: 0 EOF # 导出公钥:用 ASCII armor 文本格式,方便发给对方 gpg --export --armor recv@demo.local > recipient_pub.asc # 导出私钥:二进制格式,Java 侧用 Bouncy Castle 读取 gpg --export-secret-keys recv@demo.local > recipient_sec.gpg

参数说明:Key-Type/Key-Length控制主钥算法与长度,这里用 RSA 2048 只为联调快;Subkey-Type/Subkey-Length配置加密子钥,Expire-Date: 0表示永不过期。如果甲方要求 3072 或 4096,改长度即可,BC 侧代码不用动。私钥导出后要注意保管,.gpg文件泄露等于签名权限泄露,联调结束后要立即替换。

3. 核心代码拆解:PGP 加解密与签名验签的调用顺序

3.1 用 GnuPG 生成密钥对,还是 Java 生成?

前面说了命令行是推荐路径,但确实有人问:Demo 必须依赖 gpg 吗?答案是不需要。BC 提供了JcaPGPKeyPairGenerator,纯 Java 也能生成密钥环,只是组装主钥 + 加密子钥并打包成PGPSecretKeyRing的代码比较绕,非必要别在生产写。

这里放一段纯 Java 生成密钥对的骨架,用于理解 BC 的密钥模型:

JcaPGPKeyPairGenerator kpGen = new JcaPGPKeyPairGenerator( PublicKeyAlgorithmTags.RSA_GENERAL, new JcaPGPDigestCalculatorProviderBuilder().build()); kpGen.initialize( new RSAKeyGenParameterSpec(2048, BigInteger.valueOf(65537)), new SecureRandom()); PGPKeyPair signKey = kpGen.generateKeyPair(); // 主钥:签名 PGPKeyPair encKey = kpGen.generateKeyPair(); // 子钥:加密 // 密钥环生成器:把主钥、子钥、User ID 捆成一个标准 PGP 密钥环 PGPKeyRingGenerator ringGen = new PGPKeyRingGenerator( PGPSignature.POSITIVE_CERTIFICATION, signKey, "recv@demo.local", new JcaPGPDigestCalculatorProviderBuilder().build(), null, null, new JcaPGPContentSignerBuilder( signKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA256), new JcePBESecretKeyEncryptorBuilder(PBEEncryptionAlgorithmTags.AES_128) .setProvider("BC") .build(passphrase.toCharArray())); PGPSecretKeyRing secRing = ringGen.generateSecretKeyRing(); PGPPublicKeyRing pubRing = ringGen.generatePublicKeyRing();

逻辑说明:RSAKeyGenParameterSpec(2048, 65537)中的 65537 是 RSA 常用的公钥指数,固定值不用改。POSITIVE_CERTIFICATION表示主钥对子钥做正向自签名,这是互操作的基本要求,PGP 兼容实现都认这个类型。最后一个JcePBESecretKeyEncryptorBuilder给私钥加 passphrase 加密,生产环境必须有,否则私钥文件泄露就是裸奔。

实际联调我仍然建议 gpg 生成密钥:gpg 生成的密钥环经过了最广泛的兼容性验证,还能指定 Key-Grip、Preferences 等 BC 不暴露的细节。Java 侧只负责读。

3.2 加密并签名:流的嵌套顺序决定了能否互通

PGP 加密在 BC 里的本质是流的嵌套,顺序是固定的:最外层加密,往里是压缩,再往里是签名,最里层是明文数据(LiteralData)。这个次序反了(先加密再签名)会让接收方先验签后解密,GnuPG 直接拒绝处理。

Demo 核心方法拆出来看:

public static byte[] signAndEncrypt(byte[] data, PGPPublicKey recipientKey, PGPPrivateKey senderKey) throws Exception { ByteArrayOutputStream result = new ByteArrayOutputStream(); // 最外层:加密。AES-128 会话密钥 + 接收方公钥包裹 PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator( new JcePGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_128) .setWithIntegrityPacket(true) .setSecureRandom(new SecureRandom()) .setProvider("BC")); encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(recipientKey)); OutputStream encOut = encGen.open(result, new byte[1024]); // 第二层:压缩。ZIP 级别兼顾速度与体积 PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator( PGPCompressedData.ZIP); OutputStream compOut = compGen.open(encOut, new byte[1024]); // 第三层:签名。SHA256 + RSA 私钥 PGPSignatureGenerator sigGen = new PGPSignatureGenerator( new JcaPGPContentSignerBuilder(recipientKey.getAlgorithm(), HashAlgorithmTags.SHA256).setProvider("BC")); sigGen.init(PGPSignature.BINARY_DOCUMENT, senderKey); // 最内层:写入明文数据,同时喂给签名器 PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator(); OutputStream litOut = literalGen.open(compOut, PGPLiteralData.BINARY, "payload", data.length, new Date()); sigGen.update(data); // 先把内容喂给签名哈希 litOut.write(data); // 再写入明文层 litOut.close(); // 关闭明文层 sigGen.generate().encode(compOut); // 签名包追加到压缩流尾部 compOut.close(); encOut.close(); // 最后关闭加密层,补全密文尾包 return result.toByteArray(); }

参数说明与逻辑:SymmetricKeyAlgorithmTags.AES_128是会话密钥算法,接口报文短、性能无压力,追求安全可换 AES_256。setWithIntegrityPacket(true)会加一个校验包,解密时若报integrity check failed多半与它有关,必须保持开启。四个open()严格按加密→压缩→签名→明文嵌套,关闭要逆序,最后encOut.close()才会把加密尾包写完,漏关会导致密文不完整。对 10MB 以上报文,把open()里的 1024 buffer 调到 4096 或 8192,能明显减少内部循环调用次数。

3.3 解密并验签:反过来剥,注意校验主钥

解密是上面流程的逆过程:最外层解出会话密钥 → 解压 → 验签 → 读出明文。BC 里有PGPObjectFactory逐层 unpack 的标准套路:

public static byte[] decryptAndVerify(byte[] encrypted, PGPPrivateKey recipientPrivKey, PGPPublicKey senderPubKey) throws Exception { PGPObjectFactory factory = new JcaPGPObjectFactory(encrypted); PGPEncryptedDataList encList = (PGPEncryptedDataList) factory.nextObject(); PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) encList.get(0); // 用接收方私钥解出会话密钥,进而解出内层数据 InputStream clearData = encData.getDataStream( new JcePublicKeyDataDecryptorFactoryBuilder() .setProvider("BC").build(recipientPrivKey)); PGPObjectFactory innerFactory = new JcaPGPObjectFactory(clearData); // 第二层:压缩包 PGPCompressedData compData = (PGPCompressedData) innerFactory.nextObject(); innerFactory = new JcaPGPObjectFactory(compData.getDataStream()); // 第三层:签名列表 PGPSignatureList sigList = (PGPSignatureList) innerFactory.nextObject(); PGPSignature signature = sigList.get(0); // 第四层:明文数据,边读边喂给验签器 PGPLiteralData literalData = (PGPLiteralData) innerFactory.nextObject(); InputStream dataStream = literalData.getInputStream(); signature.init(new JcaPGPContentVerifierBuilderProvider() .setProvider("BC"), senderPubKey); ByteArrayOutputStream plain = new ByteArrayOutputStream(); int ch; while ((ch = dataStream.read()) >= 0) { plain.write(ch); signature.update((byte) ch); // 验签哈希必须按字节流实时算 } boolean ok = signature.verify(); // false 说明签名无效或公钥不匹配 return plain.toByteArray(); }

逻辑说明:getDataStream内部会自动匹配密钥环里对应的子钥,前提是私钥对象确实包含目标子钥,否则抛 "No PGP key found"。验签时signature.update必须在读明文的同时进行,因为 PGP 签名哈希按字节流实时计算,verify()只是最后比对。若ok == false,第一怀疑对象是公钥拿错了,第二才是数据被篡改。

这套解密代码两个隐藏点:一是多层JcaPGPObjectFactory要逐层新建,不能复用同一个工厂实例,否则索引流位置错乱;二是PGPLiteralData的文件名和长度字段只是调试信息,业务解析不需要理会。

4. 接进 HTTP Client 与 HTTPS:PGP 报文传输层的三个易错细节

4.1 HttpClient 发送 PGP 报文:Content-Type 与实体构造

PGP 加密后的报文是二进制字节流,可以直接 POST 给接收方。细节在 Content-Type:很多接口用application/pgp-encrypted,也有用application/octet-stream的,必须跟对方约定一致,否则部分网关会做文本化转换,导致密文错乱。

Demo 里用 Apache HttpClient 4.5 发送的核心代码:

CloseableHttpClient client = HttpClients.custom() .setSSLContext(sslContext) // 第 4.2 节构建 .setConnectionTimeToLive(30, TimeUnit.SECONDS) .setRetryHandler(new DefaultHttpRequestRetryHandler(2, true)) .build(); HttpPost post = new HttpPost("https://gw.demo.local/v1/transfer"); post.setHeader("Content-Type", "application/pgp-encrypted; charset=UTF-8"); post.setEntity(new ByteArrayEntity(pgpPayload)); // 直接传 byte[],不做字符串转换 post.setHeader("X-Request-Id", UUID.randomUUID().toString()); CloseableHttpResponse resp = client.execute(post); byte[] respBody = EntityUtils.toByteArray(resp.getEntity());

逻辑说明:setSSLContext注入第 4.2 节构建的上下文,单向双向取决于对方要求。setConnectionTimeToLive(30, SECONDS)控制连接池里空闲连接存活时间,PGP 加密通常几十到几百毫秒,这一项不会成为瓶颈,长连接场景可放宽到 60 秒。DefaultHttpRequestRetryHandler(2, true)的第二个参数表示只对幂等请求自动重试,POST 不在其列,所以真正的重试只能靠业务层幂等——X-Request-Id就是干这个的:接收方遇到相同 request id 直接返回上一次结果,避免报文重复入账。

new ByteArrayEntity(pgpPayload)直接传加密后的 byte[],这是最稳妥的做法。踩过坑的人都知道:如果先new String(pgpPayload, "UTF-8")再发,二进制字节在 JSON 序列化或编码转换时很容易被破坏。

4.2 SSLContext 构建:单向 HTTPS 和双向证书的加载差异

PGP 与 HTTPS 是两个层次的加密:PGP 保护报文内容,解决"数据被看、被改";HTTPS 保护通道,解决"链路被窃听、中间人"。接口里两者叠加才是完整链路,不是二选一。

单向认证(最常见):客户端只校验服务端证书。

KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); try (InputStream in = new FileInputStream("/etc/app/truststore.jks")) { trustStore.load(in, "changeit".toCharArray()); } TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore); SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());

双向认证(mTLS):除信任服务端证书外,还要加载自己的私钥证书。

KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm()); kmf.init(clientKeyStore, "keystorepass".toCharArray()); // 含私钥的 JKS/P12 SSLContext mTlsContext = SSLContext.getInstance("TLSv1.2"); mTlsContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
维度单向 HTTPS双向 HTTPS(mTLS)
客户端证书不需要需要含私钥的 JKS/P12
服务端证书校验TrustManagerTrustManager
KeyManagernull必须初始化
适用场景大多数外部接口金融、政务等高安全网关

注意SSLContext.getInstance("TLSv1.2"):现在多数网关已支持 TLS 1.3,如果对方明确只支持 1.2,用这个没问题;假如直接写默认 TLS 版本,某些 JDK 会协商到 1.3 反而被网关拒绝,这种报错信息千奇百怪,容易让人误以为是 PGP 的问题。

4.3 超时、重试与幂等:PGP 加解密耗时对调用链的影响

PGP 加密不是零成本,RSA 2048 加解密 10KB 报文大约 5~20ms,但加上四层流嵌套和 JCA Provider 首加载,首次调用可能冲到 200ms 以上。很多人联调时超时设置沿用默认,结果首调用直接 socket timeout。

给一组参考值:

参数建议值理由
connectTimeout3000ms连不上尽快报错
socketTimeout10000ms留出 PGP 加解密 + 网络往返余量
connectionRequestTimeout5000ms连接池获取不阻塞太久
业务重试次数2 次幂等重试配合 X-Request-Id 防重复

在 HttpClient 4.5 里的写法是RequestConfig.custom().setConnectTimeout(3000)...,挂到 client 的setDefaultRequestConfig上即可。这里还要留意 JVM 堆内存:报文压到 100MB 级时,PGP 解密会同时存在密文流、明文流、签名流三份数据,堆余量要按报文体积 3 倍估算,细节在第 5 章展开。

5. 避坑手册:PGP 实战里最容易翻车的五个现场

5.1 坑一:公钥是 ASCII armor,却用二进制流去解析

现象:读对方发的.asc文件,PGPObjectFactory解析出来是 null,或者直接抛IOException,报错指向 "unknown object in stream"。

原因:.asc是 Base64 编码的 ASCII armor 文本格式,开头带-----BEGIN PGP PUBLIC KEY BLOCK-----;.gpg/.pgp才是二进制 DER 格式。BC 的PGPObjectFactory对二进制流友好,遇到 armor 文本直接懵。

解决:先做 armor 解码再交给工厂。常见做法是检测文件头后调用PGPUtil.getDecoderStream:

public static PGPPublicKey readPublicKey(String filePath) throws Exception { try (InputStream in = new FileInputStream(filePath)) { // getDecoderStream 自动识别 armor 或二进制格式 InputStream decoded = PGPUtil.getDecoderStream(in); PGPPublicKeyRingCollection rings = new PGPPublicKeyRingCollection(decoded, new JcaKeyFingerprintCalculator()); for (PGPPublicKeyRing ring : rings.getKeyRings()) { for (PGPPublicKey key : ring.getPublicKeys()) { if (key.isEncryptionKey()) { // 只取加密子钥 return key; } } } } throw new IllegalArgumentException("no encryption key in file: " + filePath); }

PGPUtil.getDecoderStream会读前几个字节判断格式,实战兼容性最好。isEncryptionKey()这行也关键——很多示例不写,直接取主钥,结果主钥算法是RSA_SIGN,加密阶段报 "key is not suitable for encryption"。

5.2 坑二:解密报 "No PGP key found",密钥 ID 过滤忘了写

现象:加密、发送都成功,接收方解密时抛org.bouncycastle.openpgp.PGPException: No PGP key found。

原因:私钥环里有多个子钥,而密文头记录的是加密子钥的 Key ID,与主钥 ID 完全不同。如果只把整个私钥环传给解密器,它不知道用哪个 Key ID 索引,自然找不到。

解决:解密前先解析密文的 Key ID,再精确取私钥:

PGPEncryptedDataList encList = (PGPEncryptedDataList) new JcaPGPObjectFactory(encrypted).nextObject(); PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) encList.get(0); long keyId = encData.getKeyID(); PGPSecretKey secretKey = secretKeyRingCollection.getSecretKey(keyId); PGPPrivateKey privateKey = secretKey.extractPrivateKey( new JcePBESecretKeyDecryptorBuilder() .setProvider("BC") .build("yourpass".toCharArray()));

getSecretKey(keyId)是标准做法,不要自己遍历比对。多写这三行,能省掉大量"密钥没错、就是解不开"的排查时间。

5.3 坑三:字符编码不统一,加密没报错、解出来是乱码

现象:Windows 上跑 Demo,加密解密全程无异常,但对方解出来的中文变成"锟斤拷",或者反过来我方解密对方报文中文乱码。

原因:PGP 只处理字节,不知道字符集。发送方str.getBytes()用了平台默认编码(Windows 是 GBK),接收方new String(bytes)用 UTF-8,中文自然乱。这个坑不报任何异常,最阴。

解决:所有字符到字节的转换统一 UTF-8。加密前显式data.getBytes(StandardCharsets.UTF_8),解密后new String(plain, StandardCharsets.UTF_8),HTTP 头再声明charset=UTF-8提醒对方。血泪经验:业务代码里永远不要调无参getBytes(),这是生产环境最高频的编码事故源。

5.4 坑四:Bouncy Castle 版本冲突,NoSuchMethodError

现象:启动即报NoSuchMethodError或NoClassDefFoundError,栈顶指向org.bouncycastle.openpgp.*,但 pom 里明明引了 BC 1.70。

原因:项目里另一个依赖(老版本加密 SDK、旧版安全组件)传递性引入了bcprov-jdk15on老版本,classpath 里存在两套 BC 类。OpenPGP API 在 1.46 到 1.70 之间接口有删改,运行期加载到旧类就炸。

解决:先看依赖树再统一版本:

mvn dependency:tree -Dincludes=org.bouncycastle:bcpg-jdk15on -Dverbose

确认冲突来源后,用dependencyManagement统一版本,或把污染源用<exclusions>排掉。如果实在排不掉,退路是用 maven-shade-plugin 把 BC 重新 relocation 到自定义包名下,避开与其他组件的类冲突。

5.5 坑五:大报文一把梭,堆内存被打爆

现象:报文从 10KB 涨到 200MB,跑一次加密直接OutOfMemoryError: Java heap space,或者 GC 频繁到接口超时。

原因:Demo 里几乎都用的ByteArrayOutputStream加byte[]一次性处理,明文、密文、签名流同时驻留堆内,200MB 报文实际占用可能到 600MB 以上,-Xmx不够就崩。

解决:换流式处理,避免大 byte[]。加密端把内存流换成FileOutputStream,或直接用 BC 自带的文件写入 API:

// 大文件加密:流式写入,不经过内存 PGPUtil.writeFileToLiteralData(litOut, PGPLiteralData.BINARY, new File(payloadFile));

解密端对应地用PGPLiteralData.getInputStream()边读边写磁盘,不要在内存里readAllBytes()。内存模板是密文流、明文流、签名更新流三条流同时开,生产 JVM 至少留出报文体积 3 倍的堆余量,这是很多人容易忽略的黑匣子指标。

6. 端到端验证:联调清单与加解密耗时量化

拿到这份 Demo 之后别急着改业务代码,先按下面五步做一轮端到端自检,每个环节能独立验证再往下走,能省掉一半联调时间:

步骤做什么结果
1生成发送方/接收方两套密钥,相互交换公钥公钥文件能解析出加密子钥
2发送方加密并签名一段中文报文密文头部能看到 PGP 版本头
3发送方自己解密自己加密的报文中文不乱码、验签通过
4用 HttpClient POST 给本地 mock 服务mock 能收到完整密文,不走 HTTPS 也能解
5加 HTTPS/mTLS,全程抓包确认链路握手成功,报文被通道和 PGP 双重加密

耗时量化的一个技巧:不要用System.currentTimeMillis()做单次测量,JVM 预热和 JCA Provider 首次加载会污染结果。我一般用System.nanoTime()连续跑 100 次,取中位数和 P99:

long[] costs = new long[100]; for (int i = 0; i < costs.length; i++) { long start = System.nanoTime(); signAndEncrypt(payload, encKey, signKey); costs[i] = (System.nanoTime() - start) / 1_000_000; } Arrays.sort(costs); // costs[49] 是中位耗时,costs[98] 是 P99 耗时

RSA 2048 + AES-128 加密 10KB 报文,中位数一般在 8~20ms;解密验签比加密略贵,因为验签是模幂运算,大约 20~40ms(JDK 对 RSA 私钥操作有 CRT 优化,实际看 CPU)。如果 P99 超过 200ms,优先查 GC 和堆内存,其次再看流嵌套里的 buffer 大小。

这套 Demo 我拆过不止一次,之后养成了一个习惯:每次改完加密相关代码,强制走一遍"自己加密 → 自己解密 → 自己验签"的闭环,再上网关联调。别嫌麻烦,PGP 的流嵌套和密钥 ID 问题,八成只在闭环测试里暴露。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询