ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JDK升级后JCE无法认证Provider BC的排查与修复

JDK升级后JCE无法认证Provider BC的排查与修复 上周把一套还在跑的老服务从 JDK 8 挪到 JDK 17本地mvn clean package之后跑得好好的加解密逻辑一进容器就抛SecurityException: JCE cannot authenticate the provider BC日志里前面还跟着一串at javax.crypto.JceSecurity.verifyProviderJar。这个报错在 JDK、SecurityException、JCE、provider BC 这几个关键词上搜索量一直不低因为它踩的是 JDK 升级里最容易被忽略的一块——不是语法不兼容、不是模块不兼容而是 JCE 的provider 认证机制在换版本之后换了判定条件。它解决起来不难但难在很多人第一反应是BC 版本太老换个新的结果换了三四个版本依然报同一个错白白耗掉半天。按我这些年的经验遇到这个异常的基本都是三类人一是把老项目从 JDK 8 往 JDK 11/17/21 上迁的二是用了 shade、assembly 之类插件把依赖打成一个大 jar 的三是在容器里自己管 JDK 环境的。这篇文章就把这条报错从头到尾拆开它到底在校验什么、升级 JDK 之后哪几个条件变了、四条修法各自适合什么场景、怎么一步步复现和定位、以及我自己踩过的那些坑。看完你应该能自己判断手上的工程属于哪一类而不是挨个试版本。1. 先把这条异常读懂JCE cannot authenticate the provider BC 到底在说什么1.1 异常抛出点与调用链还原这条异常不是你写的代码抛的是 JDK 内部抛的位置在javax.crypto.JceSecurity这个类里。它的完整链路是这样的你在代码里调了Security.addProvider(new BouncyCastleProvider())这一步通常不报错真正触发校验的是第一次使用该 provider 提供的加密服务比如Cipher.getInstance(SM4/ECB/PKCS5Padding, BC)。这时 JCE 会去拿 provider 的校验结果如果缓存里没有就走一遍校验流程校验失败就抛出SecurityException。关键点在于Security.addProvider只是把 provider 注册进java.security.Security这张表里它本身不做任何签名校验。很多人看到注册时就该报错的直觉是错的所以会出现启动日志没有异常一调加密接口才炸的现象排查时容易误判成业务代码问题。理解了这一点你就能明白为什么同一个 BC 版本有的接口能跑、有的接口一调就挂——取决于该接口有没有真正走到 BC 的 provider 上。校验流程的核心方法是JceSecurity.verifyProviderJar(URL codeBase)。它做的事情是先拿到 BC 的BouncyCastleProvider这个类是从哪个 jar 加载的也就是ProtectionDomain里的CodeSource然后打开那个 jar逐个 class 条目检查它们是否都被完整签名任何一个条目没有有效签名整个 provider 就判定为不可认证。抛异常时会带上 provider 的名字所以我们看到的就是固定的JCE cannot authenticate the provider BC这个字符串。这里有个细节值得记住如果codeBase拿不到也就是 provider 类的CodeSource为 nullJCE 会直接放行不做校验。这个分支是后面所有绕过式方案的理论基础也是老一代把 jar 丢到扩展目录技巧能生效的原因。反过来讲只要codeBase是一个正常的 jar 文件 URL校验就躲不掉。1.2 JCE 认证机制的设计初衷搞清楚它为什么存在比记命令有用得多。JCE 是 Java 的加密扩展在很长一段历史时期里加密算法的强度是受到出口管制的弱强度的实现可以随便用强强度的实现需要单独授权。JDK 的做法就是自带的 SunJCE provider 由官方签名第三方 provider 必须提供一个可验证的签名 jarJCE 靠这个签名来确认这是一个被认可的加密实现而不是随便谁塞进来的一段代码。所以这套机制的本质是一个来源可信性检查只不过它检查的是 jar 的签名完整性而不是检查功能对不对。这解释了一个经常让人困惑的现象明明我的 BC 能正常跑 AES 加解密为什么 JCE 还要拦我因为拦你的不是 BC是 JCE 这个守门人它拦的是这个 jar 的签名状态。顺着这个设计意图往下想就能推出两件事。第一官方发布的bcprov是带签名的正常情况下不会有问题。第二任何会破坏签名的操作——重打包、合并 jar、手动改 jar 内容——都会把这个认证打破哪怕类字节码本身一个字节都没变。这就是为什么我什么都没改只升级了 JDK这个描述通常是不准确的中间一定发生了打包方式或者加载方式的变化。1.3 从 JDK 8 到 JDK 17这套认证是怎么变的JDK 8 到 JDK 11 之间这套机制在校验什么上没有根本性变化但在什么情况下跳过校验上变化很大这才是升级后集中爆发的根源。JDK 8 时代jre/lib/ext是真实存在且生效的扩展目录。你把bcprov-jdk15on-xxx.jar往这个目录一扔JVM 的扩展类加载器就把它加载进来codeBase的判定会走可信分支签名校验直接略过。当年大量项目就是靠这一招解决的配置文件里躺着一句把 BC 放进 jre/lib/ext的注释一躺就是好几年谁也没意识到这是个隐患。从 JDK 9 开始扩展机制被标记为废弃并给出警告到 JDK 11jre目录整体消失java.ext.dirs这个系统属性不再影响类的加载-Djava.ext.dirs...也基本失效。于是原来那个扔进 ext 就完事的路径彻底断了。如果此时你的工程又把 BC 打进了 fat jar签名在打包过程中被破坏那两条路一起断报错就必然出现。这也是我遇到过的绝大多数案例的真实成因。再补一句 JDK 9 之后的模块化影响BC 的类被放到了未命名模块里这本身不影响签名校验但会让CodeSource的取值更干净——一定是那个具体的 jar 文件。以前在某些容器里CodeSource可能是 null 从而侥幸绕过现在绕不过去了等于把原来被掩盖的问题暴露出来。所以严格来说不是升级 JDK 引入了 bug而是升级 JDK 把一直存在的隐患照了出来。2. 升级 JDK 之后集中爆发的四类根因2.1 根因一jre/lib/ext 扩展目录在 JDK 11 之后彻底消失这是出现频率最高的一类。典型特征是你去翻老项目的部署脚本或者运维文档能找到类似将 bcprov 拷贝至$JAVA_HOME/jre/lib/ext这样一条步骤而新环境的 JDK 目录下压根没有jre这个子目录。这时候 BC 要么根本加载不到报ClassNotFoundException要么因为你同时在 pom 里引了 BC从 classpath 加载签名校验上线报 SecurityException。要确认是不是这一类可以做个很直接的检查。执行java -XshowSettings:properties -version 21 | grep -i ext.dir在 JDK 8 上会打印出java.ext.dirs ...且指向真实的jre/lib/ext在 JDK 11 之后的版本上这个属性要么不存在要么指向一个与扩展加载无关的位置。这个命令我很推荐三十秒就能排除掉一大类猜测。需要提醒的是不要试图用-Djava.ext.dirs/some/path在新 JDK 上救回来。JDK 9 里这个属性已经被忽略JDK 11 之后完全没有效果写了只会让你误以为配好了。正确的心态是扩展目录这条路径已经在现代 JDK 上不存在了必须换一种加载方式或解决签名问题而不是找它的替代品。还有一个变体有人把 BC 的 jar 放进了容器的共享 classpath 目录比如某些中间件的lib/ext或者自建的common/lib。这在 JDK 8 上可能侥幸能跑因为共享 classpath 由特定类加载器装载判定分支不同升级之后类的加载路径变了codeBase变成了具体 jar校验就又上线了。这类问题最隐蔽因为配置文件看起来什么都没动。2.2 根因二shade / assembly 重打包把 BC 的签名文件弄没了第二类是高发区尤其在微服务和一个大 jar 走天下的部署模式里。BC 的官方 jar 里带有一组签名相关的元数据文件位置在META-INF/下常见的文件名是BCxxx.SF、BCxxx.RSA或BCxxx.DSA还有META-INF/MANIFEST.MF里与之对应的一批摘要条目。JCE 校验时会读这些文件逐个条目核对摘要。重打包插件的行为是把各个依赖 jar 解开把 class 文件按包名合并到同一个目录树然后重新压成一个 jar。这个过程里签名文件要么被直接丢弃很多插件默认排除META-INF/*.SF这类文件要么被保留但内容已经对不上了因为 class 被重新组织、其他依赖的摘要也混在一起。两种结果对 JCE 来说都是致命的前者是完全没有签名后者是签名存在但验证不通过。这里有个特别容易误判的点很多人发现修复的第一步是在 shade 配置里排除签名文件照做之后错误依旧于是怀疑方案不对。其实排除签名文件只是为了让重打包过程不报冲突它本身不能解决 JCE 认证问题——一个完全没有签名的大 jarJCE 照样拒绝。正确的完整链条是先排除旧签名再对整个大 jar 重新签名第二步才是关键漏掉它等于白干。顺带说一个对照情况Spring Boot 的spring-boot-maven-plugin用的是嵌套 jar 布局各个依赖以原始 jar 的形式放在BOOT-INF/lib/下BC 的 jar 是原封不动搬进去的签名文件完好所以通常不会触发这个异常。如果你的项目正好是这种布局却依然报错那问题多半不在打包而在别处比如 classloader 展开或者自定义了加载逻辑。把这个对照记在心里能帮你快速判断我这算不算重打包问题。2.3 根因三BC jar 命名与 JDK 版本对不上号BC 的构件命名有个历史包袱早年的bcprov-jdk15on表示适配 JDK 1.5 及以上bcprov-jdk15to18表示适配 1.5 到 1.8后来主流换成了bcprov-jdk18on意思是适配 JDK 1.8 及以上。名字里那个数字是最低兼容版本不是只能用在某个版本。这一点很多人理解反了以为jdk15on就是给 JDK 15 用的。虽然命名本身不会直接导致 SecurityException但它会间接把你带进坑里。典型场景是升级 JDK 时顺手把 BC 从bcprov-jdk15on换成了某个更新的版本同时打包方式没变结果原来那个签名文件被排除的老配置还在新版本 jar 的签名结构不同处理逻辑对不上异常依旧。另一种是保留了很老的 BC 版本那个版本发布的 jar 签名算法已经过时某些 JDK 上用默认参数重新签名后校验会失败。我的建议很直接JDK 8 及以上统一用bcprov-jdk18on系列或者用 LTS 分支bcprov-lts8on别在jdk15on这类老命名上纠结。选版本的时候也不要去追最新的那个数字选一个比你的 JDK 发布稍晚一点的稳定版就够了太新的版本有可能用到你 JDK 还不支持的语言特性反而引入新问题。版本对照我在第 5 节整理了一张表可以直接对着查。2.4 根因四继承 BouncyCastleProvider 的自定义类把签名带偏这一类知道的人少但确实存在。有些项目为了修改默认配置会写一个class MyProvider extends BouncyCastleProvider然后在里面调put(...)改参数或者注册自定义算法最后Security.addProvider(new MyProvider())。问题在于MyProvider这个类是你自己工程编译出来的它在你的 jar 里不在 BC 的签名 jar 里。JCE 校验时取的是 provider 实例的类也就是MyProvider的CodeSource而不是父类所在的 BC jar。于是校验对象变成了你的业务 jar——这个 jar 显然没有 BC 的签名认证自然失败。这类问题的迷惑性在于报错信息里 provider 名字可能还是BC因为你在构造函数里设了名字让人完全想不到是自定义类惹的祸。修法有两种。稳妥的是别继承改成组合先Security.addProvider(new BouncyCastleProvider())注册官方实例需要改配置时通过系统属性比如Security.setProperty或者在使用处显式指定参数不要动 provider 的类继承关系。另一种是接受现实把自定义 provider 所在的那个 jar 一起重新签名让签名覆盖到它。选哪种取决于你的自定义程度如果只是改几个参数强烈建议走第一种。3. 方案选型四条路线怎么挑3.1 四条路线横向对比在动手之前先把选项摆清楚比一头扎进其中一个方案要高效得多。按投入成本和适用场景我把它们分成四条路线下面这张表是我自己选型时会过一遍的对照。路线核心动作适用场景改动成本主要风险保留官方签名不做重打包BC 以独立 jar 加载Spring Boot 嵌套布局、能改打包方式的工程低需要调整构建链路重新签名剥离旧签名后对整个产物签名必须打单一 fat jar 的场景中需管理密钥、签名环节易漏独立加载用外部 lib 目录 classpath 扩展容器化部署、运维可控中classloader 关系要理清弃用 BC改用 JDK 内置 provider 或自研实现只用 AES/RSA/ECDSA 这类通用算法低到中国密等算法无法替代选型时问自己三个问题就够了这个工程必须打成单 jar 吗运维能不能接受多一个 lib 目录业务里有没有强依赖 BC 独有算法比如 SM 系列、Ed25519 早期版本、某些 PGP 相关能力三个问题的答案基本就锁定了路线。我个人的偏好顺序是能用嵌套 jar 就用嵌套 jar因为它零成本且不留后患不能的话优先重新签名再不行才考虑独立 lib 目录因为多一个目录意味着多一处部署失误的可能最后才是弃用 BC因为改算法是业务级改动风险和成本都不低。3.2 路线一保留官方签名 jar别做重打包这条路线的核心思想是别碰 BC 的 jar。只要 BC 从官方发布的、带签名的 jar 加载JCE 认证天然通过你不需要做任何额外配置。听起来像废话但它恰恰是大多数人绕了一大圈之后回到的答案。具体怎么做取决于你的构建工具。如果是 Spring Boot确认用的是spring-boot-maven-plugin的repackage目标而不是 shade 插件并且没有开layoutNONE。后者会把嵌套 jar 展开直接把签名问题带回来。如果是普通 Java 应用把 shade 或 assembly 插件换成maven-dependency-plugin把依赖复制到target/lib启动时用java -cp app.jar:lib/*或者对应的 classpath 通配写法。容器化场景下这个方案尤其顺手镜像里本来就有一层放依赖的目录把 BC 的原始 jar 单独放进去即可。需要注意别让构建工具在复制过程中顺手过滤掉META-INF目录某些企业内部的构件仓库或者镜像构建脚本会做这类清洗这是个隐蔽的坑。复制完之后用jarsigner -verify抽查一下能省掉后面很多来回。3.3 路线二用 jarsigner 自己重新签名当工程确实必须产出单一 fat jar 时重新签名是最干净的做法。这里有个原理必须讲清楚否则你不会相信自己签的也能过JCE 的 jar 校验用的是 jar 里携带的签名者证书公钥来做签名有效性验证它验证的是这个 jar 的内容和签名自洽并不要求证书链条能追溯到系统的cacerts信任库。所以自建密钥签出来的 jar在 JCE 眼里和官方签的没有区别。这个特性意味着你不需要去买证书也不需要 BC 官方的私钥自己生成一个 keystore 就够。操作分三步先把大 jar 里残留的旧签名元数据剥掉再用keytool生成密钥最后用jarsigner对整个 jar 签名。顺序不能反先签再剥等于没签。命令细节我在第 4 节完整给出来。需要提醒的是密钥和口令的管理别偷懒放在代码库里。这类签名密钥一旦泄露别人可以伪造你产物的签名虽然在这个场景下危害有限但规范上不该开这个口子。我一般用 CI 的环境变量注入本地开发用一个单独的测试密钥两边分开。另外签名的 digest 算法建议显式指定 SHA-256 及以上别用默认值老版本的jarsigner默认可能还是 SHA-1某些 JDK 会拒绝。3.4 路线三让 BC 以独立 jar 的形式加载这条路线的适用场景是构建产物不好改但部署环节你说了算。做法是把 BC 的原始 jar 放在应用的 classpath 之外的一个固定目录通过启动参数把它加进 classpath。比如java -cp app.jar:/opt/app/lib/bcprov-jdk18on-1.78.jar com.example.Main或者用 manifest 里的Class-Path条目声明。关键在于原始 jar这四个字。如果你放在那个目录里的 jar 本身就已经被重打包过了签名早没了这条路也走不通。所以这个方案通常要配合前一个方案一起用先保证手里有一个带原始签名的 BC jar再想办法让 JVM 从它加载。要验证效果可以在代码里打印一下new BouncyCastleProvider().getClass().getProtectionDomain().getCodeSource().getLocation()看看输出的路径是不是你放的那个原始 jar。如果不是说明加载路径被别的东西抢先了常见原因是工程依赖树里还藏着一份被打包的 BCclassloader 先加载了它。用mvn dependency:tree -Dincludesorg.bouncycastle查一遍把重复依赖排掉。3.5 路线四能不用 BC 就不用这条经常被忽略但值得先评估。如果你的项目用 BC 只是为了 AES、RSA、ECDSA、SHA 这类通用算法那 JDK 自带的 provider 完全够用而且从 JDK 8u161 起AES-256 这类强算法的密钥长度限制已经默认放开不需要额外策略文件。这种情况下把 BC 摘掉报错自然消失还顺带少了一个依赖和一层签名约束。不能摘的典型场景是国密算法SM2/SM3/SM4、某些老版本的 Ed25519JDK 15 之后内置之前需要 BC、部分 PGP 相关能力以及一些小众的椭圆曲线。这些确实只能靠 BC 或者自研实现。评估的方法很简单全局搜一下代码里所有用了BC作为 provider 名的位置逐个看算法名判断 JDK 内置能不能替代。这项工作一次做完收益是长期的。如果只是部分算法依赖 BC还有个折中方案把 BC 的使用范围收窄到只走那几个独有算法的代码路径其余走默认 provider。这样即使 BC 的加载出问题核心业务链路也不至于全挂。这个思路在稳定性要求高的系统里挺实用我一直推荐。4. 手把手实操从复现到修好4.1 搭一个最小复现工程排查这种东西先把手上的大工程放一边用最小工程复现出问题再回到大工程改效率高得多。最小区分点在于BC 从哪加载和签名是否完整所以这个最小工程要能自由控制打包方式。先写一小段探针代码注意这里选 SM4 是为了确保必须走 BC因为 JDK 内置 provider 不提供这个算法能排除掉其实没用 BC的干扰import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.security.Security; public class BcProbe { public static void main(String[] args) throws Exception { Security.addProvider(new BouncyCastleProvider()); System.out.println(provider Security.getProvider(BC)); System.out.println(code source BouncyCastleProvider.class.getProtectionDomain() .getCodeSource().getLocation()); Cipher c Cipher.getInstance(SM4/ECB/PKCS5Padding, BC); c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(new byte[16], SM4)); byte[] out c.doFinal(hello.getBytes()); System.out.println(out len out.length); } }先用一个普通的mvn dependency配置依赖org.bouncycastle:bcprov-jdk18on用exec:java直接跑正常应该不报错code source指向你本地仓库里那个原始 jar。这一步是基线确认环境本身没问题。然后把依赖换成通过 shade 插件打一个 fat jar再用java -jar跑同一段代码。如果 JCE 校验的逻辑成立这里就应该复现出JCE cannot authenticate the provider BC。如果没复现说明你的 shade 配置恰好保留了签名文件那是另一回事可以手动删掉META-INF/*.SF再跑一次确认。这个对照实验做完你对根因的把握就非常确定了。4.2 三步定位签名到底丢在哪复现出来之后用三条命令就能把签名丢在哪个环节定位清楚。第一条看产物里到底还有没有签名文件jar tf target/app.jar | grep -Ei META-INF/.*\.(SF|RSA|DSA|EC)这条命令没有输出说明签名元数据已经被丢掉有输出也不能掉以轻心要看文件名和内容是否对应得上。第二条用jarsigner验证整个 jar 的签名状态jarsigner -verify -verbose -certs target/app.jar | tail -n 20注意这里的判定标准看到jar verified只能说明签名自洽不代表 JCE 会接受看到jar is unsigned就是明确没有签名如果出现大量entry is not signed说明只有部分条目被覆盖这在 JCE 眼里同样是不合格的。我踩过的一个坑就是只看最后一行jar verified就以为没问题实际上中间混着未签名的条目跑了半天才发现。第三条看运行时的实际加载来源用上面探针里的那行code source输出即可。这一步是确认JVM 到底从哪个 jar 加载 BC很多人修了半天签名结果 JVM 加载的根本不是被修的那个 jar白费功夫。三条命令走完你手上就有了完整的事实链。4.3 重新签名的完整命令与验证确认要走重新签名这条路之后按下面的顺序操作。先准备一个密钥有效期给长一点避免以后换密钥还得重新过一遍流程keytool -genkeypair \ -alias build-signer \ -keyalg RSA -keysize 2048 -validity 3650 \ -keystore build-sign-ks.p12 -storetype PKCS12 \ -storepass your-store-pass \ -dname CNinternal-build, OUplatform, Oexample, CCN这里的-keyalg RSA -keysize 2048是必须显式指定的不要用默认值某些 JDK 的默认密钥算法和长度会让后续签名算法选择出问题。-validity 3650是十年够用。接着剥掉产物里的旧签名残留。这一步用zip命令最直接注意通配符要加引号不然会被 shell 提前展开zip -d target/app.jar \ META-INF/*.SF META-INF/*.RSA META-INF/*.DSA META-INF/*.EC如果zip报zip error: Nothing to do说明这些文件本来就不存在可以放心继续。然后执行签名把 digest 和签名算法都显式写出来jarsigner \ -keystore build-sign-ks.p12 -storetype PKCS12 \ -storepass your-store-pass \ -digestalg SHA-256 -sigalg SHA256withRSA \ -signedjar target/app-signed.jar target/app.jar build-signer签完之后立刻验证这一步不能省jarsigner -verify -verbose -certs target/app-signed.jar | tail -n 5看到jar verified且提示签名时间是你刚刚操作的时间就对了。然后用探针里的 fat jar 方式再跑一次异常应该消失。如果还是报按上一节的三条命令重新走一遍大概率是哪个环节没生效最常见的是签名之后又被另一个构建步骤覆盖了产物注意检查构建顺序。4.4 打包插件的针对性改法手动命令只是验证思路真正落地要写进构建配置。Maven 下用 shade 的情形配置里需要两件事先把旧签名排除掉再用签名插件对整个产物签名。排除部分长这样filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude excludeMETA-INF/*.EC/exclude /excludes /filter /filters不排除的后果是 shade 过程中可能因为同名签名文件冲突而报错或者保留下来一堆对不上的旧摘要。排除之后产物变成完全无签名状态必须接上签名步骤plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jarsigner-plugin/artifactId version3.1.0/version executions execution idsign-artifact/id phasepackage/phase goalsgoalsign/goal/goals /execution /executions configuration keystore${project.basedir}/build/build-sign-ks.p12/keystore storetypePKCS12/storetype storepass${env.BUILD_KS_PASS}/storepass aliasbuild-signer/alias keypass${env.BUILD_KS_PASS}/keypass arguments argument-digestalg/argumentargumentSHA-256/argument argument-sigalg/argumentargumentSHA256withRSA/argument /arguments /configuration /plugin关键点是phasepackage/phase要跟 shade 的执行阶段一致而且执行顺序要在 shade 之后。Maven 里同一个 phase 下多个插件的顺序按声明顺序走所以签名插件的声明位置要放在 shade 后面。这一点特别容易忽略配置写对了但顺序错了签的是没有 shade 的那个原始 jar上线照样报错。Gradle 场景稍微麻烦一点官方的signing插件主要是给发布到仓库用的对已存在的 jar 重新签名需要写一段自定义任务去调jarsigner或者用shadowJar配一个doLast里执行ant.signjar。不管用哪种方式思路都一样先剥离后签名顺序不能反并且要确保签的是最终产物。注意如果你们的 CI 里有多阶段构建比如先打 jar 再打镜像请确认签名发生在 jar 打包阶段而不是镜像层。镜像层的操作不会改变 jar 内容但如果镜像构建过程会解压再压缩签名依然可能失效。5. 常见问题与排查速查表5.1 报错现象对照速查不同的现象组合指向不同的根因先把这张对照表过一遍能省不少试错时间。现象最可能的根因首选排查动作升级 JDK 后立刻报代码没动jre/lib/ext 失效用-XshowSettings:properties看 ext.dirs只有 fat jar 报IDE 里不报重打包破坏签名jar tf查 META-INF 签名文件换 BC 版本后依旧报错打包配置没改jarsigner -verify看签名状态报错信息里 provider 名是自定义的继承了 BouncyCastleProvider查getClass()的实际来源部分接口报、部分不报走了不同 provider搜代码里所有BC的用法报ClassNotFoundException而非本异常jar 根本没加载上查 classpath 和依赖树这张表的用法是从现象倒推不要从理论正推。实践中我见过太多人先入为主认定肯定是版本问题然后花几小时换版本实际上连签名状态都没看过一眼。先用表里的动作确认事实再决定改哪里。5.2 几个容易踩的坑第一个坑是只看jarsigner -verify的最后一行。前面提过输出里出现零星entry is not signed时整体依然可能显示jar verified但 JCE 的要求是全部条目都有有效签名一个漏网都不行。我的做法是加一个grep -c is not signed计数不为零就当成失败处理。第二个坑是签名之后又跑了别的打包步骤把结果覆盖了。典型链条是mvn package里 shade 生成了app.jar然后另一个插件又基于 classes 目录重新打了一遍把签好名的产物覆盖。排查方式是比对最终产物的修改时间和签名时间或者直接对最终产物再验一次。构建链路复杂的时候我会在签名任务的输出里打一行标记方便确认它确实作用在了目标文件上。第三个坑是以为Security.addProvider会立刻抛异常。前面说过它不会所以当你看到启动日志干净、接口才报错时不要怀疑日志级别或者异常被吞了。正确的排查动作是在启动阶段主动调一次 BC 提供的服务比如拿一个 SM4 的 Cipher 实例把问题提前暴露出来。我在关键服务里都会加这么一段自检上线前就能发现而不是等业务流量打上来才炸。第四个坑是重打包时只排除*.SF和*.RSA漏了*.DSA和*.EC。老版本的 BC 可能用 DSA 签名一些新版本用 EC 签名漏掉一种就会在 shade 时报冲突。四类扩展名一起排除省心。还有一个不太算坑但值得说的点重新签名后jarsigner -verify会提示签名证书不受信任这是正常的因为你用的是自签密钥。不要因为这个提示以为签名没成功。同理-certs输出里出现警告不代表 JCE 会拒绝JCE 只看签名是否自洽不看证书信任链。这个区别搞清楚了你就不会被误导。5.3 JDK 与 BC 版本对照最后把版本选择这事说清楚免得在选型阶段反复纠结。BC 构件命名含义建议使用场景bcprov-jdk15on适配 JDK 1.5 及以上老命名仅在维护极老工程时保留bcprov-jdk15to18适配 JDK 1.5 到 1.8明确只在 JDK 8 上跑时可选bcprov-jdk18on适配 JDK 1.8 及以上JDK 8/11/17/21 的默认选择bcprov-lts8onJDK 8 起的长周期维护分支对稳定性要求高于新特性的场景选版本的实操建议是先确定你集群里最低的 JDK 版本然后挑一个比这个 JDK 发布时间稍晚的 BC 稳定版。不要直接上最新的那个大版本号新版本可能有 API 调整或者对 JDK 有更高要求你的工程未必吃得消。升级 BC 跟升级 JDK 一样最好一次只动一个变量动完立刻用第 4.1 节的探针验证一遍确认没有新的异常。如果你正在做 JDK 8 到 17 的整体迁移我建议把 BC 的调整和打包方式的调整放在同一次变更里完成而不是分两次。原因很简单这两件事耦合在一起分开做的话第一次调完可能仍然报错你会误以为方向错了来回折腾的成本远超一次做完。变更完之后把BC 从哪个 jar 加载这个信息固化到健康检查或者启动日志里以后升级 JDK 时一眼就能看出有没有问题。我个人在几次类似迁移里最深的体会是这个报错表面上属于依赖问题实际上考的是你对构建产物形态的掌控力。同样是new BouncyCastleProvider()在 IDE 里跑、在普通 jar 里跑、在 fat jar 里跑、在容器里跑加载路径完全不一样而 JCE 只认其中一个。所以与其记解决方案不如养成一个习惯——任何涉及加密 provider 的工程都先确认它的CodeSource指向哪里把那行打印留在启动自检里。多做这一步后面能少掉很多次深夜排查。另外提醒一句签名用的密钥别用测试环境的那把去签生产产物虽然这个场景下证书信任不影响 JCE 校验但把环境的密钥边界划清楚总是对的。
返回列表