集成 Frimi payment gateway 时出现 SSLException
结论
SSLException: Connection closed by peer 表示 TLS/SSL 连接被对端服务器或中间网关主动关闭。这个异常通常出现在 HTTPS 握手阶段,所以应先排查连接和证书问题,而不是 grant_type 或 JSON 解析。常见原因有:
- 客户端与服务器支持的 TLS 版本或加密套件不兼容;
- UAT 服务器的证书链不完整,或 Android 设备不信任其 CA;
- 接口要求双向 TLS(mTLS),但客户端没有提供证书;
- 请求来源 IP 不在白名单中;
- 代理、WAF 或 API Gateway 根据 SNI、协议或访问策略直接断开连接;
- UAT 地址、端口或服务器不可用。
示例中的 Basic 凭据已经出现在客户端代码里,应立即到支付平台后台撤销并重新生成。支付网关的 client_secret 不应保存在 Android APK 中,access token 应由自己的后端服务器申请。
先确认问题发生在哪一层
TLS 握手发生在 HTTP 请求发送之前。如果异常由下面这行代码抛出:
Response response = client.newCall(request).execute();
并且没有收到 HTTP 状态码,服务器很可能还没有处理 grant_type、请求体或 Authorization 内容。
先在电脑或后端服务器上测试同一个地址:
curl -v \
-X POST \
'https://uatapi.nationstrust.com:8243/token' \
-H 'Authorization: Basic <BASE64_CLIENT_ID_AND_SECRET>' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials'
然后检查 TLS 握手和证书链:
openssl s_client \
-connect uatapi.nationstrust.com:8243 \
-servername uatapi.nationstrust.com \
-showcerts
检查以下内容:
- TLS 握手能否完成;
- 服务端返回的证书是否与域名匹配;
- 证书链是否完整;
- 是否出现
handshake failure、unknown ca或no peer certificate; - 服务端是否要求客户端证书;
- 换一个网络后,相同请求能否成功。
如果 curl 和 openssl 也失败,问题通常出在支付平台、证书、IP 白名单或网络链路。可以把测试时间、出口 IP 和握手日志交给 Frimi/银行接口支持人员核查。
如果电脑上可以成功,而 Android 上失败,应检查 Android 版本、OkHttp 版本、TLS 能力和证书信任配置。
按 OAuth 规范构造请求
当前代码把 grant_type 放在 URL 查询参数中,同时发送了空的表单请求体。这通常不会直接导致 TLS 连接中断,但部分 API Gateway 会严格校验表单格式。建议改用 OkHttp 的 FormBody:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
RequestBody body = new FormBody.Builder()
.add("grant_type", "client_credentials")
.build();
Request request = new Request.Builder()
.url("https://uatapi.nationstrust.com:8243/token")
.header("Authorization", Credentials.basic(clientId, clientSecret))
.header("Accept", "application/json")
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
ResponseBody responseBody = response.body();
String responseText = responseBody != null ? responseBody.string() : "";
if (!response.isSuccessful()) {
throw new IOException(
"Token request failed: HTTP " + response.code() + ", body=" + responseText
);
}
// 解析 responseText 中的 access_token
}
Credentials.basic(clientId, clientSecret) 会按照 Basic Authentication 格式生成请求头,可以避免手动拼接或重复编码。具体参数仍需以支付平台提供的 UAT 接口文档为准。
检查 TLS 版本和 Android 兼容性
较旧的 Android 系统可能没有默认启用服务器要求的 TLS 版本,也可能缺少较新的根证书。如果接口只接受 TLS 1.2,而设备仍在使用较旧的协议,服务器可能会直接终止握手。
测试时可以限制 OkHttp 使用 TLS 1.2,确认问题是否出在协议协商:
ConnectionSpec tls12Spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build();
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Arrays.asList(tls12Spec))
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
不要随意指定某个加密套件。如果客户端指定的套件不受服务器支持,握手同样会失败。更稳妥的处理方式是升级到适合项目环境的 OkHttp 版本,并确认最低 Android 版本具备所需的 TLS 能力。
还要检查设备的日期和时间。如果系统时间严重不准,证书可能被判定为尚未生效或已经过期。
检查证书链和 mTLS 要求
UAT 环境使用的证书或访问策略经常与生产环境不同,常见情况包括:
- 服务器没有发送完整的中间证书链;
- 服务器使用企业内部 CA;
- 接口要求安装客户端证书;
- 服务器只允许白名单 IP 访问。
如果服务器证书链不完整,应由服务方修复证书配置。不要使用“信任所有证书”的 TrustManager,也不要关闭主机名校验。这些做法会让 HTTPS 失去身份验证能力,并带来中间人攻击风险。
如果接口要求 mTLS,需要服务方提供客户端证书、私钥格式和接入说明,再通过 KeyStore 和 SSLContext 配置 OkHttp。只添加服务器 CA 证书不能代替客户端证书。
增加握手诊断信息
可以临时安装 EventListener,确认连接是在 DNS、TCP 连接还是 TLS 握手阶段被关闭:
OkHttpClient client = new OkHttpClient.Builder()
.eventListener(new EventListener() {
@Override
public void secureConnectStart(Call call) {
Log.d("TLS", "TLS handshake started");
}
@Override
public void secureConnectEnd(Call call, Handshake handshake) {
Log.d("TLS", "TLS version: " + handshake.tlsVersion());
Log.d("TLS", "Cipher suite: " + handshake.cipherSuite());
}
@Override
public void callFailed(Call call, IOException ioe) {
Log.e("TLS", "Request failed", ioe);
}
})
.build();
同时记录完整的异常链:
try {
// execute request
} catch (SSLHandshakeException e) {
Log.e("TLS", "TLS handshake failed", e);
} catch (SSLPeerUnverifiedException e) {
Log.e("TLS", "Certificate or hostname verification failed", e);
} catch (SSLException e) {
Log.e("TLS", "TLS connection failed", e);
} catch (IOException e) {
Log.e("HTTP", "Network request failed", e);
}
如果 secureConnectEnd() 一直没有执行,说明握手没有完成。如果握手完成后连接才中断,还需要检查 API Gateway、HTTP 协议协商和请求策略。
推荐的处理顺序
- 立即撤销示例中暴露的 Basic 凭据,并生成新凭据。
- 不要在 Android 客户端保存
client_secret,改由业务后端申请 token。 - 使用
curl -v和openssl s_client检查 UAT 地址、端口和证书链。 - 向服务方确认 TLS 版本、mTLS、IP 白名单和 UAT 服务状态。
- 使用
FormBody,把grant_type=client_credentials放入请求体。 - 在不同 Android 版本和不同网络下测试,记录完整的异常堆栈。
- 如果只有旧设备失败,检查 OkHttp 版本、TLS 1.2 支持和系统根证书。
- 不要通过信任所有证书、禁用主机名校验或改用明文 HTTP 来绕过问题。
单纯增加超时时间通常无法解决这个异常,因为对端是在主动关闭连接,并不是客户端等待响应超时。
备注:内容仅供参考。
