PAYATHON 2026

集成 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 环境使用的证书或访问策略经常与生产环境不同,常见情况包括:

  1. 服务器没有发送完整的中间证书链;
  2. 服务器使用企业内部 CA;
  3. 接口要求安装客户端证书;
  4. 服务器只允许白名单 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 协议协商和请求策略。

推荐的处理顺序

  1. 立即撤销示例中暴露的 Basic 凭据,并生成新凭据。
  2. 不要在 Android 客户端保存 client_secret,改由业务后端申请 token。
  3. 使用 curl -v 和 openssl s_client 检查 UAT 地址、端口和证书链。
  4. 向服务方确认 TLS 版本、mTLS、IP 白名单和 UAT 服务状态。
  5. 使用 FormBody,把 grant_type=client_credentials 放入请求体。
  6. 在不同 Android 版本和不同网络下测试,记录完整的异常堆栈。
  7. 如果只有旧设备失败,检查 OkHttp 版本、TLS 1.2 支持和系统根证书。
  8. 不要通过信任所有证书、禁用主机名校验或改用明文 HTTP 来绕过问题。

单纯增加超时时间通常无法解决这个异常,因为对端是在主动关闭连接,并不是客户端等待响应超时。

备注:内容仅供参考。