Java 与 LAMP 之间出现 SSL error,如何解决?
结论
页面并非不存在。问题在于 payment gateway 的 Java 客户端无法为网站证书建立受信任的证书链。浏览器能正常打开页面,SSL Labs 评分正常,也不代表对方 JVM 使用的信任库能够验证这张证书。
首先应检查网站服务器是否发送了完整的中间证书:
- 如果服务器没有发送完整证书链,应由网站方修复。
- 如果证书链完整,但对方使用了过旧或自定义的 Java truststore,则需要 payment gateway 更新 JVM、CA 信任库或连接配置。
- 不要通过关闭证书验证来绕过问题,支付流程尤其不能这样处理。
错误的含义
异常中的关键信息是:
PKIX path building failed
unable to find valid certification path to requested target
进行 TLS 握手时,Java 需要从网站证书逐级找到受信任的根证书。证书路径大致如下:
网站证书 → 中间 CA 证书 → 根 CA 证书
如果 Java 无法构建这条路径,就会抛出 SSLHandshakeException。常见原因有:
- Web 服务器没有发送中间证书,或者证书链的顺序或内容有误。
- 对方 JVM 的 CA 信任库过旧,不信任当前根证书。
- 对方使用了单独的自定义 truststore,而不是操作系统或浏览器的证书库。
- 证书由私有 CA 或内部 CA 签发,但相应 CA 尚未导入对方的 truststore。
- 对方通过代理、防火墙或 TLS 检查设备访问网站,实际收到的是另一张证书。
- 同一域名指向多个节点,但各节点的证书链配置不一致。
- 对方没有正确发送 SNI,因此收到了虚拟主机的默认证书。
这条错误指向证书信任路径,不能直接说明问题出在 TLS 版本、加密套件或页面 URL。
先检查服务器发送的证书链
在可以访问该域名的环境中执行:
openssl s_client \
-connect www.example.com:443 \
-servername www.example.com \
-showcerts \
-verify_return_error </dev/null
重点检查最后的验证结果:
Verify return code: 0 (ok)
还要确认输出中包含网站证书和必要的中间 CA 证书。服务器通常不需要发送根证书,但必须提供客户端构建信任路径所需的中间证书。
也可以单独测试 IPv4、IPv6 或具体节点,以排除负载均衡后端配置不一致的问题:
openssl s_client \
-connect 203.0.113.10:443 \
-servername www.example.com \
-showcerts \
-verify_return_error </dev/null
如果域名经过 CDN、反向代理或负载均衡,应检查所有对外节点,不能只测试源站。
修复 Apache 的证书链配置
Apache 应使用包含网站证书和中间证书的完整链文件。以下是常见的 PEM 部署方式:
<VirtualHost *:443>
ServerName www.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/example/fullchain.pem
SSLCertificateKeyFile /etc/ssl/example/privkey.pem
</VirtualHost>
fullchain.pem 通常按以下顺序组成:
-----BEGIN CERTIFICATE-----
网站证书
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
中间 CA 证书
-----END CERTIFICATE-----
具体文件名和 Apache 配置方式取决于证书签发机构、Apache 版本和部署环境。修改后先检查配置:
apachectl configtest
确认无误后重新加载服务:
systemctl reload apache2
部分发行版使用的服务名称是 httpd:
systemctl reload httpd
修复后应再次通过 openssl s_client 验证,不能只看浏览器能否正常访问。
为什么浏览器能打开,Java 却失败
浏览器和 Java 使用的信任机制可能完全不同:
- 浏览器可能使用操作系统证书库,Java 通常使用自己的
cacerts或自定义 truststore。 - 浏览器可能缓存了中间证书。
- 某些浏览器可以通过证书中的 AIA 地址下载缺少的中间证书,Java 客户端未必会这样处理。
- 浏览器和在线测试工具可能已经信任较新的根 CA,而旧 JVM 的信任库尚未收录。
- 浏览器会正确发送 SNI,某些旧客户端或配置错误的 Java 程序则可能不会。
所以,浏览器正常只能说明浏览器完成了证书验证,不能据此排除服务器证书链或 Java truststore 的问题。
如果证书链完整,需要 payment gateway 检查什么
如果从多个网络和节点测试后,都确认服务器发送了正确的完整证书链,应向 payment gateway 提供以下信息:
- 错误发生的准确时间和请求 URL。
- 网站证书的 SHA-256 指纹。
- 完整的
openssl s_client输出。 - 证书链中各证书的 Subject、Issuer 和有效期。
- 域名对应的 IPv4、IPv6 和 CDN 节点信息。
- 近期是否更换过证书、CA、CDN 或中间证书。
对方需要检查:
- Java 运行时版本及其 CA 信任库是否过旧。
- 应用是否通过
javax.net.ssl.trustStore指定了自定义 truststore。 - truststore 中是否缺少所需的根 CA 或私有 CA。
- 应用实际连接的 IP、代理和证书是否与公开测试结果一致。
- 应用是否正确发送 SNI,以及是否使用了错误的主机名。
- 多台网关服务器的 JVM 和 truststore 配置是否一致。
如需确认 Java 实际使用的 truststore,可让对方检查启动参数:
java -XshowSettings:properties -version
以及以下系统属性:
javax.net.ssl.trustStore
javax.net.ssl.trustStoreType
javax.net.ssl.trustStorePassword
还可以临时开启 Java TLS 调试日志:
java -Djavax.net.debug=ssl,handshake,certpath -jar application.jar
调试日志可能包含域名、证书和连接信息,不应直接公开。
必要时导入 CA 证书
如果网站使用私有 CA,或者对方确实使用受控的自定义 truststore,应由 truststore 的维护方导入相应的 CA 证书:
keytool -importcert \
-alias example-intermediate-ca \
-file intermediate-ca.pem \
-keystore gateway-truststore.p12 \
-storetype PKCS12
导入前必须核对证书来源和 SHA-256 指纹:
openssl x509 \
-in intermediate-ca.pem \
-noout \
-subject \
-issuer \
-fingerprint \
-sha256
应优先导入可信的 CA 证书,不要长期把单张网站叶证书加入 truststore。网站证书更新后,直接信任叶证书的配置很容易再次失效。
也不要在未经评估的情况下直接修改全局 cacerts。通常更稳妥的做法是维护应用专用的 truststore,并清楚记录其中的信任项。
不建议采用的绕过方式
不要使用信任所有证书的 TrustManager,不要关闭主机名验证,也不要把支付回调改为未加密的 HTTP。以下做法会使中间人攻击成为可能:
// 不要在生产环境中使用信任所有证书的 TrustManager。
关闭验证也许能暂时让握手通过,但 HTTPS 将无法验证服务器身份。在支付场景中,这种处理方式不能作为修复方案。
责任如何判断
责任归属要根据证书链测试结果判断:
- 服务器没有发送完整的中间证书,应由网站、CDN 或负载均衡的配置方修复。
- 只有 payment gateway 失败,而标准客户端能够完整验证服务器证书链,问题大概率在 gateway 的 JVM、truststore、代理或 SNI 配置。
- 如果使用私有 CA,双方需要协调信任证书的安全分发和更新。
- 如果只有部分请求失败,应优先检查多节点配置、IPv4 和 IPv6、代理路径以及网关集群之间的差异。
处理时,先验证所有对外节点是否发送了完整证书链,再把证书链、指纹和复现时间交给 payment gateway。确认服务器端配置无误后,才能较有把握地判断问题来自对方的 Java 环境。
备注:内容仅供参考。
