资讯动态

JSch 0.2.x升级指南:bcprov依赖变化与冲突排查全解析

发布时间:2026/10/18 5:50:53 来源:尧图企业网站定制
1. 升级前后JSch 0.2.x 的依赖到底改了什么先说结论这次升级最核心的变化就一句话bcprov 从可选依赖变成了强制依赖而且从 jdk15on 分支切到了 jdk18on 分支。你别小看这一行字真到了升级的时候光是这一个变化就能让一堆老项目在启动时直接报NoClassDefFoundError而且报错的位置往往和你实际用到 JSch 的代码八竿子打不着排查起来特别绕。1.1 旧版 0.1.x 时代是怎么引依赖的如果你是从 0.1.x 年代一路用过来的应该记得 JSch 的 Maven 坐标里有个很有争议的设计——bcprov-jdk15on是被标记为optionaltrue/optional的。什么意思呢就是 JSch 自己编译的时候需要这个库但它不强迫你也用这个版本你项目里如果已经有别的 Bouncy Castle 版本Maven 不会因为你引了 JSch 就硬塞给你一个版本冲突。那时候 JSch 的定位是尽量轻量尤其是不想因为一个加密库把整个依赖树都搞得臃肿。可是问题也出在这在早期 JDK 版本里很多 SSH 的密钥交换算法、AES 加密实现其实并不需要 Bouncy Castle——JSch 自己有纯 Java 实现兜底。所以很多人用了很久 JSch从来没注意过 bcprov 的存在直到碰到某些特定场景使用ssh-rsa之外的签名算法比如ecdsa-sha2-nistp256。客户端需要和某些强制要求特定 KEX 算法的老旧设备对接。使用基于证书的认证或者更冷门的密码学原语。一旦触发这些代码路径JSch 才会在运行期尝试加载 Bouncy Castle 的类。加载不到那就抛异常。这种平时用不到、遇到问题才炸的特性导致很多老项目里 JSch 和 bcprov 的版本关系极其混乱有人直接排除掉有人锁死老版本有人被传递依赖搞得焦头烂额。1.2 新版 0.2.x 的依赖策略调整到了 0.2.0 版本JSch 的维护者做了个比较彻底的决定把 Bouncy Castle 从 optional 改成了 compile 级别依赖并且把坐标直接从bcprov-jdk15on换成了bcprov-jdk18on。我用 Maven 实际拉了一下 0.2.15 的依赖树核心依赖信息大概是这样的com.github.mwiede:jsch:0.2.15 └── org.bouncycastle:bcprov-jdk18on:1.77 └── (传递依赖)要注意几个关键点第一bcprov-jdk18on的 groupId 还是org.bouncycastle但 artifactId 从bcprov-jdk15on变成了bcprov-jdk18on。这不是简单的版本号升级而是 Bouncy Castle 官方在 1.70 版本左右做的一次大版本分支迁徙。他们把所有面向 JDK 1.8 以上平台的构建统一放到了 jdk18on 这个分支里。第二JSch 0.2.x 内部对 Bouncy Castle 的依赖是强制的。也就是说你哪怕只是拿 JSch 连一个最普通的、只支持ssh-rsa的 SFTP 服务器它也会在类加载阶段去初始化 Bouncy Castle 相关的类。我的实测结果是只要 classpath 里没有 bcprovJSch 的初始化就会直接失败而不是以前那种用到才炸的懒加载模式。第三JSch 0.2.x 的最低要求是 Java 8。这和 bcprov-jdk18on 的要求是一致的但如果你项目还在用 Java 7那基本就告别升级了老老实实留在 0.1.x。1.3 两个分支的差异对比我整理了一张对比表方便你直观感受这次迁移影响的维度对比项0.1.x 时代0.2.x 时代bcprov 坐标bcprov-jdk15onbcprov-jdk18on依赖属性optional可选compile强制最低 JDK1.5老版本/ 1.7Java 8Bouncy Castle 版本策略JSch 自己锁定老版本如 1.46跟随 bcprov-jdk18on 较新版本如 1.77缺失依赖时的表现运行期按需加载部分功能报错类初始化阶段直接失败病毒式依赖污染低容易被排除高会强制传递到所有下游模块这张表基本说明了为什么升级 0.2.x 会让很多部门炸锅。不是 JSch 的 API 变了多少——实际上 0.2.x 对 SSH 连接的 API 改动很小大部分老代码把坐标换掉就能跑。真正的坑全在依赖冲突上。2. 为什么 JSch 要冒这个险做强制依赖2.1 出口管制和解耦的双重压力先说一个不少老开发者才记得的背景JSch 原项目在 0.1.x 时代有个很尴尬的问题——它长期停留在 0.1.55 这个版本上多年不更新。原因是 JSch 的原始作者在日本而当时日本对加密软件的出口管制政策非常严格导致他没法持续跟进新版的 Bouncy Castle。后来项目转移到了新的维护者手里才重新活跃起来发布了 0.2.0。新维护者接手之后做了几个比较重要的决策全面转向 Maven Central 发布替代老的 jar 包直传。重新对齐 Bouncy Castle 的版本用最新的 jdk18on 分支。把依赖改为强制统一代码路径减少可选依赖带来的不确定性。你可能会问为什么之前可选依赖的设计不好因为可选依赖的本质是我编译时需要但我假设你可能不需要——这个假设在现实中几乎总是错的。只要用户遇到和加密相关的功能JSch 一定需要 bcprov但你 Maven 坐标里又没强制带结果就是每个使用者要自己去补依赖。补的时候版本怎么选没人说得准。有人用老的 jdk15on有人用新的还有人干脆用别的加密库。这种混乱带来的支持成本非常高。维护者天天在 issue 区回答为什么我的 ECDSA 不工作答案永远是你自己没加 bcprov 或者版本不对。与其反复解释不如直接在 POM 里拽着你走。2.2 jdk18on 分支到底新在哪再说说为什么是 jdk18on 而不是别的分支。Bouncy Castle 从 1.70 版本开始把构建产物按 JDK 版本分成了几类bcprov-jdk15on面向 JDK 1.5 及以上但实际维护力度在下降。bcprov-jdk18on面向 JDK 8 及以上持续维护的新分支未来的主要迭代方向。artifactId 里的18指的是 Java 18 的 API 基线吗不是容易混淆。这里的jdk18on表示JDK 1.8 及以上即 Java 8 就是它的最低支持版本往上直到最新版本都能用。Java 8 到现在依然是企业级应用的主流版本所以 Bouncy Castle 官方把主要精力都放在了这个分支上。从实际的 API 差异来看jdk18on 相比老分支做了一些底层优化包括使用java.util.Random的替代方案调整了随机数生成策略。优化了部分椭圆曲线运算的常数时间实现侧信道防护更好。更完整的算法套件实现包括最新的国家商用密码算法支持。对于 JSch 的使用者来说这些底层变化感知不到但确实让新版的整体安全性上了一个台阶。2.3 强制依赖带来的连锁反应强制依赖是把双刃剑。好处很明显以后你只要引了 JSch就一定会带 bcprov-jdk18on版本由 Maven 统一管理不会再出现缺了某个类找不到的问题。坏处也很直接如果你的项目里其他地方已经用了老的 bcprov-jdk15on那么现在会同时出现两个 Bouncy Castle 分支。两个分支的 package 前缀都是org.bouncycastle.*但类文件不兼容运行时到底加载哪一个完全取决于 classpath 顺序这种问题最难排查。更麻烦的是bcprov-jdk15on的最后一个版本停在 1.78而bcprov-jdk18on已经迭代到更高的版本号。如果你依赖的别的库仍然锁死在 jdk15on 上并且你需要同时使用这两个库那就只能在两者之间做取舍或写复杂的依赖管理规则。3. 升级实战你大概率会踩到的几个坑3.1 场景一缺少 bcprov启动直接失败最典型的情况是原本项目里把 JSch 的 bcprov 给 explicit 排除掉了。升级到 0.2.x 之后原来排除依赖的规则还在但 JSch 已经默认需要 bcprov-jdk18on于是项目启动时出现这样的报错java.lang.NoClassDefFoundError: org/bouncycastle/jce/provider/BouncyCastleProvider at com.jcraft.jsch.JSch.clinit(JSch.java:...) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory...注意看这里的JSch.clinit说明是在 JSch 类的静态初始化阶段就挂了。我看了 JSch 0.2.x 的源码它的静态代码块里直接引用了 Bouncy Castle 的BouncyCastleProvider类用来注册 JCE Provider。如果 classpath 里没有这个类类加载器直接告诉你NoClassDefFoundError。解决方式很简单确认bcprov-jdk18on在依赖树里存在mvn dependency:tree -Dincludesorg.bouncycastle如果输出为空那就在你的pom.xml里手动加dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.77/version /dependency这里我推荐直接写死版本不要省略 version 依赖 Maven 的传递解析因为一旦有别的依赖干扰最怕的就是默认可用的版本和你预期的不一致。另外提醒一句NoClassDefFoundError和ClassNotFoundException长得像但原因完全不同。前者是类加载过一次失败了或者依赖的类没找到后者是压根没有这个类。遇到NoClassDefFoundError时优先检查依赖树的完整性和 exclusions 规则。3.2 场景二版本冲突bcprov 新旧分支同时存在比缺类更恶心的是bcprov-jdk15on和bcprov-jdk18on同时躺在你的依赖树里。两个 jar 包含大量同名类但内部实现不一致谁先被加载谁说了算。这种冲突的典型表现是某些安全算法初始化时报NoSuchAlgorithmException或者更离谱的在某些环境正常、另一些环境直接崩。我遇到过一个案例本地跑单元测试没事打包部署到测试环境就报错最后发现是依赖树里一个内部工具库传递引入了 jdk15on和 JSch 的 jdk18on 撞了。排查方法用 Maven 的 dependency tree 加 grepmvn dependency:tree | grep -E bcprov|bouncycastle看到类似这样的输出就要警觉了[INFO] | - org.bouncycastle:bcprov-jdk15on:jar:1.68:compile [INFO] | \- org.bouncycastle:bcprov-jdk18on:jar:1.77:compile两种应对思路如果能升级那个传递依赖的库升级到支持 jdk18on 的版本一劳永逸。如果那个库太老升级不了用 exclusion 把它的 jdk15on 排除掉然后手动确保 jdk18on 唯一。但要注意万一那个老库强依赖 jdk15on 的某些 API 实现你贸然 exclude 可能导致这个库运行时出问题。这种情况就需要做取舍——是不是值得为了升级 JSch 0.2.x 冒着个险。我的建议是先在测试环境完整回归那个老库的功能再决定是否推进。3.3 场景三老项目还在用 JDK 7升级直接被拒JSch 0.2.x 最低要求 Java 8这一点没有任何回旋余地。如果你所在的系统还在用 JDK 7或者某个边缘服务因为历史原因锁死在 JDK 7 上那么这个升级计划要直接刹车。你可能想到一个变通方案Java 8 编译的 jar 包在 JDK 7 上运行会报java.lang.UnsupportedClassVersionError。错误信息类似java.lang.UnsupportedClassVersionError: com/jcraft/jsch/JSch Unsupported major.minor version 52.0这里的52.0就是 Java 8 编译的 class 版本号JDK 7 只能识别51.0。如果出现这个错误说明你的应用确实运行在 JDK 7 环境上。老项目如果还想用新版 JSch 怎么办我见过两种方案单独部署一个 JDK 8 的 sidecar 服务把 SFTP 操作封装成一个内部接口老项目通过 HTTP 调用不直接依赖 JSch。继续留在 0.1.x但把 JSch 的 bcprov-jdk15on 显式升到一个较新的版本比如 1.68 或 1.78提升部分安全性。注意 1.78 是 jdk15on 分支的最终版本之后不再有更新。3.4 场景四依赖没有排除干净容器部署时才炸还有一种隐蔽的情况就是本地开发一切正常打包成 fat jar 或者部署到 Tomcat 时突然NoClassDefFoundError。这类问题的根源通常不是 JSch 本身而是打包插件配置导致 Bouncy Castle 的签名信息丢失。Bouncy Castle 的 jar 包以前是带签名文件的如果你用maven-shade-plugin打 fat jarshade 默认会把多个 jar 合并但 *META-INF 下的.SF 签名文件不会自动处理容易残留多个签名文件导致 JVM 验证失败。报错一般是java.lang.SecurityException: Invalid signature file digest for Manifest main attributes解决方式很简单在 shade 插件配置里过滤掉签名文件filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude /excludes /filter /filters这个坑在新版 bcprov 上遇到的人不算多因为新版本对签名策略有调整但旧项目迁移的时候还是要注意。4. 站在十字路口四种依赖处理方案怎么选4.1 方案一跟随默认接受强制依赖如果你的项目没有别的地方使用 Bouncy Castle或者用的版本已经是 jdk18on那么简单升级 JSch 到 0.2.x什么都不用改Maven 会帮你把 bcprov-jdk18on 带进来。这是最省事的路径也对应了 0.2.x 版本维护者的本意。需要注意的唯一问题是确认项目整体 JDK 版本已经到 8 以上然后跑一遍全量测试。如果你项目里用到的其他库依赖了旧的 Bouncy Castle 版本但这个库本身也兼容 jdk18on那么 Maven 的依赖仲裁会选更高的版本大概率不会报错。4.2 方案二全局排除手动指定版本适合那些没法快速统一其他库的 Bouncy Castle 分支的项目。做法是引入 JSch 时排除掉它的 bcprov-jdk18on然后单独声明你需要的 Bouncy Castle 依赖dependency groupIdcom.github.mwiede/groupId artifactIdjsch/artifactId version0.2.15/version exclusions exclusion groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId /exclusion /exclusions /dependency再手动加你想要的dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.78/version /dependency注意JSch 0.2.x 的源码在静态初始化时直接引用了org.bouncycastle.jce.provider.BouncyCastleProvider这个类的包路径在 jdk15on 和 jdk18on 里是完全一致的所以理论上 1.78 的 jdk15on 满足 JSch 的编译期引用。但我没有在极端场景下验证过 JSch 0.2.15 的所有算法是否都能跑在 jdk15on 上特别是那种用到较新加密算法的服务器。如果你决定用这个方案务必重点测试 ECDSA、Ed25519 这些较新的密钥类型。我个人不太推荐这个方案因为它是强行缝合两个不同的依赖世界短期能跑长期还是要找机会统一。4.3 方案三升级传递依赖全局统一如果你的项目里其他库引用了 bcprov-jdk15on这个问题根源在于某些二方或三方库版本太老。那正确做法不是跟 JSch 对抗而是推动那个库升级。比如老版本的某安全框架内部用了bcprov-jdk15on:1.68你去看这个框架的新版本 release notes很可能早就切到 jdk18on 了。升级框架版本两个 bcprov 变成同一个冲突自然消失。这是最干净的方案唯一的问题在于升级框架版本本身可能带来其他改动。所以这一步需要做深入回归不能只看依赖树变绿了就完事。4.4 方案四Shade 重定位物理隔离如果你的团队比较硬核或者那个引用老 bcprov 的库实在没法升级还有一个偏方用 shade 插件把 Bouncy Castle 做一个 relocation把org.bouncycastle包重命名成org.bouncycastle.shaded。这样 JSch 用的是一份 relocated 的 bcprov老库用的是原始的 bcprov两边互不干扰。示例配置relocations relocation patternorg.bouncycastle/pattern shadedPatternorg.bouncycastle.shaded/shadedPattern /relocation /relocations然后把你 relocated 的 jar 作为 JSch 专用的依赖包发布到内部仓库JSch 通过它拿到自己的加密实现。注意shade relocation 对 JCE 这类 Provider 机制支持并不完美因为 JVM 的java.security.Security在注册 Provider 时是根据类名来的比如org.bouncycastle.jce.provider.BouncyCastleProvider。一旦类名被改掉代码里那段Security.addProvider(new BouncyCastleProvider())就会编译失败或运行期找不到类。所以这个方案实现起来比听起来复杂除非你有专门的构建维护人否则不建议轻易碰。4.5 我的建议路径结合我处理过的几个迁移项目给一个务实的优先级排序如果项目整体 JDK 8、无其他 bcprov 冲突直接升级到 0.2.x啥都不用改。如果有 bcprov-jdk15on 冲突但传递依赖的库可以升级先升级那个库再升 JSch。如果那个库锁死不能动评估这个库是否真的用到了 Bouncy Castle 的加密 API如果只是依赖树里有但代码路径从不调用用 exclusion 把它剔除手动加 jdk18on。如果那个库确实强依赖 jdk15on且没法升级不建议升级 JSch。可以留在 0.1.x或者参考方案四做物理隔离。5. 实测验证升级完成后的自检清单5.1 依赖树检查升级后先别急着写代码跑一遍依赖树确认没有双分支mvn dependency:tree -Dincludesorg.bouncycastle最理想的结果是只有bcprov-jdk18on一条路径。如果冒出了bcprov-jdk15on按上面的方案去排除或升级。5.2 运行期探针验证依赖树干净不等于运行期没事。写个最简单的探针启动时确认 Bouncy Castle Provider 注册成功import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class BcProvCheck { public static void main(String[] args) { // 如果 JSch 已触发静态初始化这里也应该已经注册过 if (Security.getProvider(BC) null) { Security.addProvider(new BouncyCastleProvider()); } System.out.println(BC Provider version: Security.getProvider(BC).getVersion()); } }如果你的 JSch 在静态代码块里注册了这里打印出来的版本就是你依赖树里 bcprov-jdk18on 的版本。如果打印结果是 null 或者抛异常说明依赖加载顺序有猫腻。5.3 端到端连接测试写一个覆盖多算法场景的 SFTP 连接测试先用 RSA 密钥连接一台普通服务器。再用 ECDSA 密钥连接另一台服务器验证曲线算法。有条件的话用 Ed25519 密钥做一次连接这是较新算法里比较挑依赖的。import com.jcraft.jsch.JSch; import com.jcraft.jsch.Session; public class SftpSmokeTest { public static void main(String[] args) throws Exception { JSch jsch new JSch(); // 这里替换成你自己的测试密钥 jsch.addIdentity(/path/to/your_test_key); Session session jsch.getSession(your_user, 127.0.0.1, 22); // 测试环境允许走密码兜底生产环境千万别这么干 session.setPassword(test_password); session.setConfig(StrictHostKeyChecking, no); session.connect(10000); System.out.println(Connected, server version: session.getServerVersion()); session.disconnect(); } }这里特别要提醒StrictHostKeyChecking 只在测试环境用no生产环境必须保持yes否则等于明文裸奔。5.4 常见问题速查症状可能原因处理方式启动时NoClassDefFoundError: BouncyCastleProviderbcprov-jdk18on 不在 classpath确认依赖坐标存在必要时手动声明UnsupportedClassVersionErrorJDK 版本低于 8升级运行环境 JDK或放弃升级 JSchNoSuchAlgorithmExceptionbcprov 新旧分支冲突算法实现被错误加载排除 jdk15on统一 jdk18onSecurityException: Invalid signature file digestfat jar 打包时签名文件残留shade 过滤 META-INF/*.SF/DSA/RSA某个老库突然报错排除了它需要的 jdk15on确认该库的 API 是否依赖 Bouncy Castle做取舍6. 升级过程中的几个额外提醒6.1 不要只改版本号就发布JSch 0.2.x 对 API 的兼容性做得不错大部分代码只需要改 Maven 坐标。但别忘了你在老版本里可能也配置过各种 Session 的 config 参数比如kex、cipher、server_host_key这些算法协商参数。不同版本对这些参数的支持范围有差异有些不会的参数名在 0.2.x 里会被识别有些则会被忽略。升级完成后建议把连接配置里手工指定的所有算法参数仔细过一遍能删的删掉让 JSch 自己走默认协商。因为 0.2.x 的默认算法列表已经比 0.1.x 丰富很多手工指定反而可能限制能力。6.2 注意 BC 版本的传递影响JSch 带上 bcprov-jdk18on 之后你的项目里如果还有别的库间接依赖 BC它们的版本解析规则会统一指向最新的版本范围。如果那个库本身和最新版 BC 有兼容问题症状可能非常诡异——不是报 BC 的错误而是那个库自己的加密行为异常。遇到这种说不清是谁的锅的问题不妨把 BC 单独降级到某个保守版本试试。比如你的依赖树里选的 BC 是 1.78但你怀疑某些库不兼容就显式锁定到 1.77 验证一下。6.3 老项目隐藏的安全负债最后说个容易被忽略的背景。很多人看到升级依赖第一反应是又要花时间调冲突但没意识到停留在老版本本身也是安全风险。JSch 0.1.55 之后几年不更新里面很多加密算法已经过时比如默认允许ssh-rsa签名密钥而 OpenSSH 8.8 之后的版本默认已经禁用了这种签名算法。实际遇到的情况是有些老系统从某个时间点开始突然连不上某些新部署的服务器了。排查原因的时候发现服务器的 OpenSSH 版本升级后RSA 签名算法的默认行为改变而客户端 JSch 还是老版本协商不出双方都认可的签名算法。与其等到线上故障再救火不如提前把 JSch 升级计划排上日程——用这次 0.2.x 的依赖调整作为启动契机至少把整个项目的加密依赖基线往前推一大步。根据我个人的迁移经验从 0.1.x 升到 0.2.x 这种事情技术难度从来不是最大障碍真正难的是把项目里所有间接依赖理清楚。如果你项目里只有 JSch 一个加密库10 分钟搞定如果还有别的安全组件建议至少预留一个下午做冲突排查和回归测试。动手前记得先把老版本的依赖树快照导出来一份存起来出了问题也好对照回滚这个习惯能给你省不少事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑