Domino managed bean 连接支付网关时报 handshake_failure
结论
DISABLE_SSLV3、SSL_DISABLE_TLS_10 和 SSLCipherSpec 主要控制 Domino 自身的 TLS/HTTP 服务,无法可靠控制 managed bean 通过 HttpsURLConnection 发起的出站 HTTPS 连接。
managed bean 运行在 Domino HTTP JVM 中,应通过 Java JSSE 显式指定 TLSv1.2。还要注意,Notes 客户端版本是 9.0.1FP10IF3,服务器则是较旧的 9.0.1FP6,两者使用的 JVM 和安全补丁可能不同。建议先把 Domino 升级到受支持且包含较新 JVM 的维护版本,再在代码中限定使用 TLS 1.2。
在连接代码中指定 TLS 1.2
不要依赖 Domino 的入站 SSL 配置。可以为当前连接创建使用 TLSv1.2 的 SSLContext:
import java.io.DataOutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, null, new SecureRandom());
URL url = new URL("https://host/endpoint");
HttpsURLConnection connection =
(HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setRequestMethod("POST");
connection.setAllowUserInteraction(false);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
byte[] requestBody = postContent.getBytes(StandardCharsets.UTF_8);
connection.setRequestProperty("Content-Type", "text/xml; charset=UTF-8");
connection.setFixedLengthStreamingMode(requestBody.length);
try {
DataOutputStream out =
new DataOutputStream(connection.getOutputStream());
try {
out.write(requestBody);
out.flush();
} finally {
out.close();
}
int responseCode = connection.getResponseCode();
// 根据 responseCode 读取响应或处理错误
} finally {
connection.disconnect();
}
TLS 连接会在调用 getOutputStream() 时才真正开始建立。因此,异常出现在创建 DataOutputStream 的那一行很正常,并不代表问题出在 DataOutputStream 本身。
原代码还使用字符数设置 Content-length:
postContent.length()
字符数不一定等于实际发送的字节数。如果内容中包含非 ASCII 字符,两者就可能不一致。改用 setFixedLengthStreamingMode(requestBody.length) 可以避免长度错误。
也可以设置 JVM HTTPS 协议属性
如果要让 HTTP JVM 中所有通过 HttpsURLConnection 建立的连接都优先限定为 TLS 1.2,可以在第一次创建 HTTPS 连接之前设置:
System.setProperty("https.protocols", "TLSv1.2");
例如:
static {
System.setProperty("https.protocols", "TLSv1.2");
}
这是 JVM 全局设置,会影响同一 Domino HTTP JVM 中的其他 XPages 应用和 managed bean。因此,一般更适合使用连接级的 SSLContext。
JSSE 的默认连接对象可能会在首次使用时缓存相关配置,所以不要等到 HTTPS 连接已经建立后再设置该属性。
先确认服务器 JVM 是否支持 TLS 1.2
可以在 Domino HTTP JVM 中临时运行以下代码进行检查:
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(null, null, null);
SSLSocket socket =
(SSLSocket) context.getSocketFactory().createSocket();
for (String protocol : socket.getSupportedProtocols()) {
System.out.println("Supported protocol: " + protocol);
}
for (String cipher : socket.getSupportedCipherSuites()) {
System.out.println("Supported cipher: " + cipher);
}
socket.close();
如果下面这行代码直接抛出 NoSuchAlgorithmException:
SSLContext.getInstance("TLSv1.2")
说明当前 Domino HTTP JVM 或其安全提供程序无法使用 TLS 1.2。仅修改代码或 NOTES.INI 解决不了这个问题,必须更新 Domino Fix Pack、JVM 或整个服务器版本。Notes 客户端能够连接,也不能证明服务器端 JVM 具备相同能力。
TLS 1.2 仍然握手失败时
handshake_failure 并不只代表协议版本不匹配。即使已经启用 TLS 1.2,支付网关仍可能因为以下原因拒绝连接:
- Domino JVM 与网关没有共同支持的 TLS 1.2 cipher suite。
- 网关要求 SNI,而旧 JVM 的 SNI 支持不符合要求。
- 网关要求客户端证书,也就是双向 TLS。
- 网关证书链使用了旧 JVM 不支持的签名算法或根证书。
- 网关只接受较新的 ECDHE、AES-GCM 等套件,而服务器 JVM 没有对应实现。
- IBM JVM 的加密策略限制了网关要求的算法或密钥长度。
可以在测试环境中临时启用 JSSE 握手日志:
System.setProperty("javax.net.debug", "ssl,handshake");
这项属性必须在第一次 TLS 连接前设置。检查日志时,可以重点关注:
- 客户端实际发送的协议版本;
ClientHello中提供的 cipher suites;- 客户端是否发送了 SNI;
- 服务器是在收到
ClientHello后立即拒绝连接,还是在证书交换阶段失败。
该调试属性同样会影响整个 HTTP JVM,而且会产生大量日志,不适合长期用于生产环境。
推荐处理顺序
- 将 Domino
9.0.1FP6更新到组织当前允许且受支持的维护版本,同时更新服务器 JVM、安全提供程序和证书库。 - 确认服务器 JVM 的
supportedProtocols中包含TLSv1.2。 - 使用连接级的
SSLContext.getInstance("TLSv1.2")。 - 如果仍然失败,临时启用 JSSE 调试,检查协议、cipher suite、SNI、证书链和客户端证书要求。
- 向支付网关确认允许的 TLS 版本、cipher suites,以及是否强制要求 SNI 或双向 TLS。
问题的重点不在于继续调整 SSLCipherSpec,而在于正确配置 Domino HTTP JVM 的出站 JSSE,并处理服务器 FP6 与客户端 FP10IF3 之间的 JVM 能力差异。
备注:内容仅供参考。
