尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JDK升级后BouncyCastle报JCE无法认证?根因与修复指南

JDK升级后BouncyCastle报JCE无法认证?根因与修复指南 把一个跑了三年的服务从 JDK 8 升到 JDK 17本地mvn test全绿一上预发就给你甩一句java.lang.SecurityException: JCE cannot authenticate the provider BC——这个报错我前后踩过三次每次的根因都不一样。它最坑的地方在于它不怪你的代码也不怪 JDK 本身它怪的是BC 这个 provider 的 jar 在 JVM 眼里是真是假。JCE 框架要求所有加密 provider 必须以可验证签名的形式存在一旦签名链对不上JDK 会直接拒绝为你提供任何基于该 provider 的加密服务包括Cipher、MessageDigest、Signature、KeyAgreement这些。这篇文章写给正在做 JDK 版本升级、又依赖 BouncyCastle 做国密SM2/SM3/SM4或者做证书、PDF 签名、TLS 加解密的同学我会把 JCE 的 provider 校验机制讲透再把我自己的排查链路、五种根因对应的修法、以及哪些看起来能修其实在埋雷的偏方全都摆出来。不管你用的是 JDK 8u 系列还是 17/21思路是通用的。1. 这行 SecurityException 到底在说什么1.1 把异常信息拆成四个部分看java.lang.SecurityException: JCE cannot authenticate the provider BC这句话信息密度其实很高只是长得太像模板报错了很多人扫一眼就跳过。我们逐段拆SecurityException这是 JDK 安全框架主动抛出的拒绝不是你调用的加密 API 自己报的错。也就是说代码压根没跑到算法实现里在准入这一关就被拦住了。JCEJava Cryptography Extension负责把 provider 提供的算法包装成Cipher、Mac、Signature这些标准接口。注意JCE 本身不实现算法它只做注册、鉴权、分发。authenticate认证。这里认证的对象不是用户是 provider 的实现类所在的 jar 包。provider BCBC是 BouncyCastleProvider 注册到 JVM 时使用的名字不是类名。如果你用Security.addProvider(new BouncyCastleProvider())名字就是BC如果有人用new BouncyCastleProvider(MYBC)注册这里的名字就会变成MYBC。这一点在排查多 provider 共存时特别有用。所以整句话的翻译是JCE 想确认 BouncyCastle 这个 provider 的实现jar是不是可信来源确认失败于是拒绝提供服务。它跟算法对不对、密钥对不对、网络通不通一毛钱关系都没有。我见过最典型的误判是有人看到这个报错就去改Cipher.getInstance(SM4/ECB/PKCS5Padding)里的算法串改成SM4/CBC/PKCS7Padding、改成AES甚至怀疑密钥格式。这些操作全是无效功——算法串再改provider 准入还是过不去。1.2 为什么它偏偏在第一次调用加密服务时才炸这一点直接决定了排查的方向。JCE 对 provider 的校验是懒加载的Security.addProvider()只是把这个 provider 对象塞进Security的 provider 列表并不会立刻去验 jar。真正触发校验的时机是第一次从该 provider 取加密服务的时候也就是javax.crypto.Cipher.getInstance(SM4/ECB/PKCS5Padding, BC); // 或者 java.security.MessageDigest.getInstance(SM3, BC); // 或者 java.security.Signature.getInstance(SM3withSM2, BC);这三行里的任意一行第一次执行时JDK 内部的sun.security.jca.JceSecurity会走到verifyProvider()这一步拿到BouncyCastleProvider这个类的CodeSource也就是它的 class 文件是从哪儿加载的打开对应的 jar 文件读取里面的签名信息校验签名是否有效、签名证书是否可被信任。校验通过就缓存通过校验失败就缓存那个异常对象后续每次调用都直接把这个异常重新抛出来。这带来两个很实际的现象第一服务启动时不报错业务跑到某个分行卡住了才报。所以日志里这个异常出现的时间点往往比真实根因升级 JDK、换打包方式晚了好几天甚至几个版本。第二失败结果在同一个 JVM 里是被缓存的。所以在同一个进程里重试一下重新addProvider一次把 provider 移除再加回来都没用——你必须要重启 JVM 才能让校验逻辑重跑一遍。我之前在线上热更新过一个 jar 想试试看白折腾了二十分钟。提示如果你需要确认是否真的走到了 provider 校验这一步加-Djava.security.debugprovider启动观察日志里 provider 的注册顺序和位置信息。这个开关在排查到底注册了哪个 BC时非常有效。2. JCE 的 provider 校验机制为什么 jar 必须原封不动2.1 校验的三要素CodeSource、签名文件、证书链JCE 的校验逻辑说白了就是三件事串起来找到这个类是从哪来的。通过BouncyCastleProvider.class.getProtectionDomain().getCodeSource().getLocation()拿到一个位置正常情况是一个file:/.../bcprov-jdk18on-1.80.jar这样的路径也可能是 URL 形式。打开这个 jar看里面有没有签名。签名信息存放在META-INF/目录下典型文件是META-INF/BC2048KE.SF、META-INF/BC2048KE.RSA这类BC 官方的签名文件命名。.SF是签名清单.RSA/.DSA是签名块本身。验证签名块里的证书链。这一步要求签名能对上 jar 内容摘要、证书在有效期内、并且能沿着证书链走到一个 JDK 认可的信任根上。JDK 认可的信任根主要来自cacerts这个信任库JDK 8 在$JAVA_HOME/jre/lib/security/cacertsJDK 9 以后在$JAVA_HOME/lib/security/cacerts。三件事里任意一件不成立都会走到同一个异常分支抛出来的都是那句一模一样的JCE cannot authenticate the provider BC。这就是为什么这个报错难查——它把至少五种不同的根因压缩成了一句话。我在实际排查里最重要的一个认知转变是不要把它当加密问题看要把它当打包与类加载问题看。它跟 jar 的内容完整性、类加载路径、jar 有没有被二次加工关系最大。2.2 证书有效期与被信任根同一个 jar 换个 JDK 为什么会废掉这是升级 JDK 后最常见的、也最反直觉的一类根因。很多项目用的 BC 版本是 1.5x 甚至更早比如bcprov-jdk15on-1.54、1.57。这类老版本的签名证书在签发时是有效的但证书本身带有效期。当 JDK 的版本升级以后JDK 内置的cacerts会跟着 JDK 一起更新——不同 JDK 小版本里包含的根证书集合是不一样的JDK 会在版本迭代里增删一些根证书。同时某些 JDK 版本对签名证书有效期的校验更严格。结果就是同一个 jar 文件物理上一字节没变在 JDK 8u144 上能跑在 JDK 8u302 或者 JDK 17 上就过不了校验。这也是 BouncyCastle 官方在那几年持续重新签名、反复发新版本的原因之一。所以看到这类报错先去看你的 BC 版本号如果连 1.60 都不到大概率就是它。第二类根因更隐蔽签名被破坏了。JCE 的校验是宁可错杀的设计。以下几种操作都会让签名失效而且失效后报错一模一样操作为什么会让签名失效用 maven-shade-plugin 打 fat jarshade 会重写 jar 内容、重排 class签名文件的摘要对不上用 spring-boot-maven-plugin 的 repackage原始 jar 被嵌进BOOT-INF/lib成为嵌套 jarCodeSource 不再是普通文件路径手工解压后又重新zip打包zip 条目顺序、压缩方式变了摘要对不上用工具裁剪META-INF减小体积签名文件和清单被删jar 变成未签名用 proguard 或混淆工具二次处理字节码变了摘要必然对不上看到这张表你就会明白为什么本地 IDE 里跑得好好的、打成包就炸是这类问题的经典表现——IDE 里用的是本地仓库里的原始 jar打包后用的是被加工过的 jar。2.3 校验结果缓存与看起来随机的现象JceSecurity内部把校验结果缓存在一个 provider 维度的 Map 里。这意味着同一个 JVM 里第一个触发校验的线程决定后续所有人的命运一旦校验失败后续所有调用都会拿到同一个异常对象堆栈也高度相似往往都指向你业务代码里第一个Cipher.getInstance的位置所以日志里那个报错位置根本没有诊断价值你真正要找的是BC 的类是从哪个 jar 加载的。有个细节值得单独说当同一个 JVM 里存在两个BouncyCastleProvider比如一个来自应用 fat jar 里的 1.57一个来自某个中间件依赖传递进来的 1.72谁先触发校验谁就成为BC这个名字的持有者。你可能改了 pom 升到 1.72但先被加载的还是老的那个——报错继续。这个问题我在第三章会给出具体的排查命令。3. 我的排查顺序从jar 有没有被改过开始3.1 先分清是环境变了还是 jar 变了升级 JDK 这件事往往会顺带改掉三样东西运行环境JDK 版本、cacerts、依赖版本顺手升了几个库、打包方式换了 Boot 版本、换了插件配置。所以第一步不是看代码是做一个二分拿老 JDK 跑同一个包如果老 JDK 不报、新 JDK 报问题在环境侧证书链、cacerts、JDK 行为变化主攻第四章的 4.1 和 4.4。拿新 JDK 跑一个最小 main 方法如果最小 main 不报、Spring Boot 包报问题在打包侧签名被破坏、嵌套 jar主攻 4.2 和 4.3。两边都报先按打包侧处理因为打包侧的问题更容易确认成本也低。我一般会写一个十几行的探针类它比任何日志都直接import java.security.CodeSource; public class BcProbe { public static void main(String[] args) throws Exception { Class? clazz Class.forName(org.bouncycastle.jce.provider.BouncyCastleProvider); System.out.println(java.version System.getProperty(java.version)); System.out.println(java.home System.getProperty(java.home)); System.out.println(classloader clazz.getClassLoader()); CodeSource cs clazz.getProtectionDomain().getCodeSource(); System.out.println(codeSource (cs null ? null : cs.getLocation())); System.out.println(signers (cs null || cs.getCodeSigners() null ? null没有拿到签名者JCE 校验基本注定失败 : cs.getCodeSigners().length 个签名者)); // 真正触发 JCE 的 provider 校验 java.security.Security.addProvider( new org.bouncycastle.jce.provider.BouncyCastleProvider()); System.out.println(provider java.security.Security.getProvider(BC)); System.out.println(cipher javax.crypto.Cipher .getInstance(SM4/ECB/PKCS5Padding, BC).getProvider().getName()); } }这里最关键的是codeSource和signers两行输出。codeSource如果是file:/.../bcprov-jdk18on-1.80.jar这种干净的路径说明加载路径正常如果出现jar:file:/app.jar!/BOOT-INF/lib/bcprov-jdk18on-1.80.jar!/这种带感叹号的嵌套路径那基本可以确定是嵌套 jar 的问题。signers如果是null说明类加载器压根没在 jar 里找到有效签名JCE 那一步必然失败除非你自己往 cacerts 里补了信任根。3.2 用 jarsigner 直接验签别猜jarsigner是 JDK 自带的它的输出比任何日志都权威。对着你实际打包产物里那个 BC jar 执行# 先看签名文件还在不在 unzip -l bcprov-jdk18on-1.80.jar | grep -E META-INF/.*\.(SF|RSA|DSA) # 再看签名是否成立注意 -certs 会把证书链打出来 jarsigner -verify -verbose -certs bcprov-jdk18on-1.80.jar输出对照表我整理成这样看着更直观jarsigner 输出关键词含义下一步jar verified.签名完整且证书链可解析问题在信任根或 JDK 侧看 4.4jar is unsigned.签名文件被删干净了打包环节把签名剥了看 4.2、4.3invalid signature file digest签名文件还在但内容对不上jar 被改写过看 4.2Warning: This jar contains entries whose certificate chain is not validated有签名但链不完整信任根缺失看 4.4Warning: This jar contains signatures that do not include a timestamp只是提醒不影响可以忽略这一步的核心价值是把签名到底有没有坏这个不确定性彻底消掉。我踩过的最深的坑就是把一个被 shade 处理过的 jar 拿来验jarsigner明确报invalid signature file digest而我当时还在怀疑 JDK 版本白折腾了一下午。3.3 确认类到底是从哪个 jar 加载的多版本共存的问题靠看 pom 是看不出来的必须看运行时。两个命令# 方式一让 JVM 打印类加载轨迹然后过滤 java -verbose:class -jar app.jar 21 | grep -i bouncycastle | head -20 # 方式二Maven 层面先排一遍只能看直接和传递依赖看不到运行时的类加载顺序 mvn dependency:tree -Dincludesorg.bouncycastle:* -Dverbose-verbose:class会告诉你org.bouncycastle.jce.provider.BouncyCastleProvider是从哪个路径加载的。如果路径指向的是某个你根本没注意过的中间件自带的 jar或者指向了BOOT-INF/lib里的嵌套路径问题就定位了。注意-verbose:class输出量很大一定要配合grep和head否则日志文件会迅速膨胀到几百兆。生产环境排查时建议单独起一个临时实例不要在主力节点上开。3.4 打开 provider 相关的调试开关看注册顺序-Djava.security.debugprovider会打印 provider 的注册与查找过程包括位置索引。用它看两件事BC这个名字被注册在第几位以及它对应的类来自哪里。如果某个 JDK 版本上这个选项输出不明显可以换成-Djava.security.debugall全开输出量极大只在临时排查时用。另外JDK 的kotlin式启发java.security.debug的取值在历史版本里有过变化jar相关项在部分版本上会打印 jar 验证的细节。这些开关不是标准化的调试接口行为可能随 JDK 版本变化所以它们只作为辅助证据最终判断还是要靠 3.1 的探针类和 3.2 的jarsigner。4. 五种根因对应的修法按优先级排4.1 版本太老升级 BC并且把 artifactId 一起换掉这是投入产出比最高的修法我一般先做这个。BC 的坐标命名里带 JDK 兼容标记升级时不能只改版本号artifactId 也得跟着换否则会出现依赖下载不到或加载了完全不同的包。运行环境推荐坐标说明JDK 8org.bouncycastle:bcprov-jdk18on:1.78.11.72 起官方提供 jdk18on面向 JDK 8 及以上JDK 8受限于字节码 1.5bcprov-jdk15to18:1.78.1保留旧字节码兼容功能分支同步JDK 11 / 17 / 21bcprov-jdk18on:1.78.1或1.80目前主流的稳定分支老项目≤1.70bcprov-jdk15on:1.701.70 是 jdk15on 的最后版本之一几个必须一起换的模块漏一个就可能出现NoSuchMethodErrorbcprov算法、bcpkix证书与 CMS、bcutil工具类、bcpgPGP、bcmail。BC 各模块之间的内部 API 在不同版本之间存在变动混版本使用非常容易在运行期炸而且是那种堆栈完全看不懂的NoSuchMethodError: org.bouncycastle.asn1.ASN1Primitive。所以在依赖收敛上我的做法是在父 pom 里用dependencyManagement统一定义所有 BC 模块的版本业务模块只声明groupId:artifactId不写版本。这样能从根本上避免某个模块偷偷带了个 1.57 进来。dependencyManagement dependencies dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78.1/version /dependency dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk18on/artifactId version1.78.1/version /dependency /dependencies /dependencyManagement换完之后一定要重新跑 3.1 的探针类确认codeSource指向的是新 jar。升级的过程中我还遇到过一件小事本地 Maven 仓库里同时存在bcprov-jdk15on和bcprov-jdk18on两个目录IDE 的索引没刷新导致编译期用的还是老包。清一次本地仓库对应目录 Reimport 就好。4.2 打包把签名搞坏了shade 与 repackage 的正确姿势如果你确认签名已经坏了第一反应不该是想办法修签名而是想办法别破坏它。因为修签名见 4.4本身有运维成本。shade 场景很多老项目的 shade 配置里有一行META-INF/*.SF、META-INF/*.RSA的 exclude这是当年为了防止程序打出来的包被别人误认为签名包而加的。但对于 BC这行配置恰恰是致命的——它把 BC 的签名删了JCE 立刻拒绝。所以对 BC 的处理原则是不要 shade 它让它以独立 jar 的形式存在。如果业务上必须打成一个包比如交付给客户的单一 jar那就接受 4.4 的重签方案或者走 4.5 绕开 JCE。Spring Boot repackage 场景spring-boot-maven-plugin默认会把依赖放进BOOT-INF/lib形成嵌套 jar。此时CodeSource变成jar:file:/app.jar!/BOOT-INF/lib/bcprov-jdk18on-1.78.1.jar!/JCE 那套基于普通JarFile的校验拿不到有效签名于是报错。绕过办法有两个我都实测过# 方案一用 PropertiesLauncher把 BC 放在外层 lib 目录注意需要把启动类换成 PropertiesLauncher java -Dloader.path./lib -cp app.jar org.springframework.boot.loader.launch.PropertiesLauncher # 方案二把 BC 的 jar 放到外层 classpath让它由 AppClassLoader 优先加载 # 注意 Application ClassLoader 是父加载器父优先委派会让外层 jar 生效 java -cp app.jar:lib/bcprov-jdk18on-1.78.1.jar:lib/bcpkix-jdk18on-1.78.1.jar \ org.springframework.boot.loader.launch.JarLauncher方案二需要说明一下原理Spring Boot 的启动类加载器最终是以AppClassLoader也就是-cp对应的加载器为父的标准委派是父优先所以放在-cp里的 BC 会被先加载CodeSource就是一个干净的file:路径。这里要小心一点BC 相关的所有 jarbcprov、bcpkix、bcutil最好都放在外层避免内外混着来出现一半从外层加载、一半从 fat jar 加载的情况。另外Spring Boot 3.2 之后启动类换到了org.springframework.boot.loader.launch包下老版本是org.springframework.boot.loader。写命令前先确认一下版本不然会直接ClassNotFoundException。4.3 类从非常规位置加载把 BC 挪回真实文件路径除了 fat jar还有几种场景会让 BC 的CodeSource变得不规矩处理方式都是同一件事——让它从真实文件加载把 BC 的 class 解压到了目录里比如有人为了减小体积把bcprov解包后塞进WEB-INF/classesCodeSource变成一个目录JarFile打不开必然失败。这个必须改回去。OSGi 容器Karaf、部分 ESBbundle 的类加载方式和 URL 协议与标准 jar 不同BC 的签名校验经常过不去。可行做法是把 BC 作为 framework 级别的 bundle 安装或者在容器层面配置信任。JVM agent / 自定义 ClassLoader某些 APM 或热部署 agent 会用自己的类加载器加载类CodeSource可能是null。这种最难查但 3.1 的探针类一跑就露馅了。判断标准很简单codeSource输出的字符串里不应该出现!嵌套、不应该以/结尾目录、不应该出现你完全没见过的路径。4.4 证书链过不去自签名并导入信任库当你确实动不了版本比如被老代码绑死在 1.5x 的 API 上又必须让它跑起来可以考虑自己给 jar 重签一遍把你自己的证书导入 JDK 的信任库。这条路是走得通的但我要把代价说清楚你的 cacerts 变成了运行时依赖每次升级 JDK、每次换容器基础镜像都要重新导入一遍运维成本不低。# 1. 先把原始签名剥掉避免旧签名干扰验证 zip -d bcprov-jdk18on-1.78.1.jar META-INF/*.SF META-INF/*.RSA META-INF/*.DSA # 2. 生成一把用于签名的密钥生产环境请用正式密钥、正式有效期、正式 DN keytool -genkeypair -alias jce-signer -keyalg RSA -keysize 2048 -validity 3650 \ -keystore jce-signer.jks -storepass 你的口令 \ -dname CNInternal JCE Signer, OUPlatform, OExample, CCN # 3. 重新签名注意指纹算法JDK 17 下建议显式指定 SHA-256 jarsigner -keystore jce-signer.jks -storepass 你的口令 \ -digestalg SHA-256 -sigalg SHA256withRSA \ bcprov-jdk18on-1.78.1.jar jce-signer # 4. 导出证书 keytool -exportcert -alias jce-signer -keystore jce-signer.jks -storepass 你的口令 \ -file jce-signer.cer # 5. 导入 JDK 信任库JDK 8 在 jre/lib/securityJDK 9 在 lib/security keytool -importcert -noprompt -alias jce-signer \ -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit \ -file jce-signer.cer # 6. 验证签名与信任链 jarsigner -verify -verbose -certs bcprov-jdk18on-1.78.1.jarJDK 9 以后可以直接用keytool -importcert -cacerts -alias ... -file ...它自己会找到当前 JDK 的信任库能少写一长串路径。这一步有几个必须知道的注意事项重签必须覆盖所有 BC 模块只重签bcprov不够bcpkix里的类同样会被 JCE 校验。zip -d那一步建议保留成脚本因为每次升级 BC 版本都要重来一遍手工操作很容易漏。cacerts默认口令是changeit很多运维基线会改掉它导入前先确认。容器化部署时不要直接改基础镜像里的 cacerts 而是通过初始化脚本导入否则换镜像就丢。更稳的做法是在 CI 里把导入步骤固化成一个固定的构建阶段。从合规角度看重签第三方 jar 属于对供应链产物的修改团队内部要走一下评审并记录签名密钥的保管方式。4.5 实在要绕开直接用 BC 的 lightweight API这是我最推荐的一劳永逸方案虽然前期有点工作量。BouncyCastle 内部其实有两套 APIJCE 层org.bouncycastle.jce.provider.BouncyCastleProvider对外暴露标准 JCE 接口受 JCE 校验约束。lightweight 层org.bouncycastle.crypto.*纯算法实现不注册 provider不经过 JCE 框架因此完全绕开签名校验问题。对于国密这类业务逻辑基本自控的场景加解密、签名验签、摘要直接换成 lightweight API 是很容易的。以 SM4-CBC 为例import org.bouncycastle.crypto.engines.SM4Engine; import org.bouncycastle.crypto.modes.CBCBlockCipher; import org.bouncycastle.crypto.paddings.PKCS7Padding; import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; public final class Sm4Raw { public static byte[] encrypt(byte[] key, byte[] iv, byte[] data) { PaddedBufferedBlockCipher cipher new PaddedBufferedBlockCipher( CBCBlockCipher.newInstance(new SM4Engine()), new PKCS7Padding()); cipher.init(true, new ParametersWithIV(new KeyParameter(key), iv)); byte[] out new byte[cipher.getOutputSize(data.length)]; int len cipher.processBytes(data, 0, data.length, out, 0); try { len cipher.doFinal(out, len); } catch (org.bouncycastle.crypto.InvalidCipherTextException e) { throw new IllegalStateException(SM4 加密失败, e); } return java.util.Arrays.copyOf(out, len); } }这套写法的优点很实在不依赖 provider 注册、不受打包方式影响、升级 JDK 也不用担心签名问题。代价是你要自己处理分组模式的拼装、填充、IV 管理如果项目里用到了CipherInputStream、SealedObject、JSSE 这类强绑定 JCE 的组件就没法完全绕开。我的实际做法是混合核心业务加解密走 lightweight证书解析和 TLS 相关还是走 JCE同时把 BC 版本维持在最新双保险。5. 几个特别隐蔽的坑5.1 classpath 里躺着两个 BC而且老的那个先被加载这是我明明升级了但还是报错最常见的原因。中间件比如某些数据库驱动、规则引擎、报表工具会传递依赖进老版本 BC。你的 pom 里写的是 1.78实际加载的可能还是 1.57。判断方法就是 3.3 的-verbose:class | grep -i bouncycastle看输出里出现了几个路径。解决方法是把老版本彻底排干净dependency groupIdcom.example/groupId artifactIdsome-middleware/artifactId exclusions exclusion groupIdorg.bouncycastle/groupId artifactId*/artifactId /exclusion /exclusions /dependency注意这里用通配符*排掉整个 groupId 的传递依赖比一个个列 artifactId 更稳。排完之后一定要重新跑一次探针类和-verbose:class因为dependency:tree只能看声明看不到运行时的父优先委派结果。5.2 升级 JDK 顺手把 java.security 和 cacerts 重置了这是我在一次 JDK 8u202 升 8u312 时踩的坑。老项目当年的做法是往$JAVA_HOME/jre/lib/security/java.security里手工加一行security.provider.11org.bouncycastle.jce.provider.BouncyCastleProvider然后靠lib/ext目录来加载 BC 的 jar。升级 JDK 时这个文件被新版本覆盖手工加的配置全丢而且 JDK 9 之后lib/ext这个扩展目录机制已经被移除了这条路彻底封死。更稳的做法是不要改 JDK 自带文件改用系统属性把自定义配置追加上去java -Djava.security.properties/opt/app/extra.security -jar app.jar单个表示把指定文件的内容追加到 JDK 默认配置之后双表示整体替换掉默认配置这个要非常小心会导致默认 provider 全丢。在extra.security里可以写 provider 顺序之类的配置升级 JDK 时不会丢。5.3 IDE 里正常、打包后炸本地正常、容器里炸这两种现象指向的是不同的问题别混在一起查现象大概率根因首选动作IDE 正常打成 jar 就报打包破坏了签名shade/repackage跑jarsigner -verify看 4.2本地打包正常进容器就报容器基础镜像的 JDK 与本地版本/构建者不同比对java -version和java.home看 4.1、4.4单机跑正常多实例里部分节点报节点上的 jar 被运维脚本改过或路径不同把节点上的 jar 拉下来做jarsigner -verify校验一直正常某天突然不报错变成报错有人改了打包配置或换了一次 base image对比镜像与构建产物的 hash最后一行的经验值得展开说一句把BC 签名完好当成一个可以自动校验的构建产物属性。我在 CI 里加了一步jarsigner -verify检查只要签名有问题直接 fail 构建。这一步不过十行脚本但把这类事故从预发才发现提前到了提交时就拦下。5.4 BC 各模块版本必须锁死这条我在 4.1 提过但值得再强调一次因为它带来的报错跟本文主题完全不同却经常同时出现。BC 的bcprov和bcpkix之间存在内部 API 依赖版本差超过一两个小版本就可能出现NoSuchMethodError或AbstractMethodError。这种报错堆栈里通常看不到任何签名相关的信息会让人误以为跟 JCE 无关实际上是同一个升级动作引起的连锁反应。我的做法是在父 pom 的dependencyManagement里定义 BC 全套版本并且在 CI 里加一条检查——运行mvn dependency:tree -Dincludesorg.bouncycastle:*的输出里所有 BC 模块的版本必须一致不一致就 fail。6. 落到工程上的几条个人经验把这三次排查串起来看我的结论是这个报错本质上是一个构建产物完整性 类加载路径的问题跟加密算法没关系跟 JDK 版本本身更是间接关系。所以我现在的处理顺序固定成了四步先跑探针类看codeSource和signers再用jarsigner -verify确认签名再看-verbose:class确认加载来源最后才去动版本或者重签。还有一个我用了之后省了很多事的习惯把 BC 相关依赖收拢到一个内部 starter 或者基础依赖模块里所有业务方都从这里继承版本和排除规则。以前每个业务组各自维护 BC 版本出了这个报错之后光你们用的哪个版本这一个问题就要来回问半天。收拢之后版本变更只在一处发生bcpkix和bcprov再也不会错配。至于长期的架构选择如果你手里的项目是新建的、并且主要用国密算法我个人倾向于从一开始就用 lightweight APIorg.bouncycastle.crypto.*把 JCE provider 这条路尽量少用。它多写几十行封装代码换来的是对 JDK 升级、打包方式、容器镜像全都免疫——考虑到 JDK 的发布节奏越来越快这笔账是划算的。而对于已经在线上跑着的老系统最务实的路径还是升级 BC 到与当前 JDK 匹配的版本同时确保它不被任何打包工具加工过。
返回列表