如何在KMM Ktor客户端中配置信任自签名SSL证书?
Ktor 客户端信任自签名证书配置方案
不要用全局禁用证书校验的野路子,会直接失去HTTPS的防中间人能力,正确方案是把自签名根证书内置到客户端,配置客户端仅信任这张证书。另外提前确认:你生成自签证书时已经把服务端静态IP填到了SAN(主题备用名称)字段,否则就算加了信任也会报主机名不匹配错误。
前置准备
把服务端的自签名根证书(crt/cer/pem格式都可以)导出,分别放到两端工程的资源目录:
- Android端:放到
app/src/main/res/raw/目录,命名比如self_signed_cert.crt - iOS端:把证书拖入iOS工程Resources组,勾选对应编译target即可,不需要在Info.plist里全局放开ATS限制。
跨层配置逻辑
因为SSL信任配置是平台相关能力,用KMM的expect/actual机制实现:
首先在commonMain里定义期望方法:
// commonMain 路径下的SslConfig.kt import io.ktor.client.* expect fun HttpClientConfig<*>.configureSelfSignedCertTrust()
在你原来的ApiGateway初始化HttpClient的代码块里,加上这行配置调用:
private val client = HttpClient { installContentNegotiation() installLogging() configureRequest() configureResponseValidator() installAuth() configureSelfSignedCertTrust() // 新增这行 }
Android端实际实现(androidMain)
Ktor Android端默认用OkHttp引擎,加载内置证书构造自定义信任管理器即可:
// androidMain 路径下的SslConfig.kt import android.content.Context import io.ktor.client.* import io.ktor.client.engine.okhttp.* import okhttp3.CertificatePinner import java.io.InputStream import java.security.KeyStore import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.net.ssl.SSLContext import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager private fun Context.buildSelfSignedTrustManager(): X509TrustManager { val certInput: InputStream = resources.openRawResource(R.raw.self_signed_cert) val certFactory = CertificateFactory.getInstance("X.509") val selfSignedCert = certFactory.generateCertificate(certInput) as X509Certificate certInput.close() val trustStore = KeyStore.getInstance(KeyStore.getDefaultType()) trustStore.load(null, null) trustStore.setCertificateEntry("server_self_signed", selfSignedCert) val tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) tmFactory.init(trustStore) return tmFactory.trustManagers.first() as X509TrustManager } actual fun HttpClientConfig<*>.configureSelfSignedCertTrust() { engine { // 替换成你项目里能拿到的Application Context val appContext: Context = /* 自行注入Application Context */ val trustManager = appContext.buildSelfSignedTrustManager() val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, arrayOf(trustManager), null) config { sslSocketFactory(sslContext.socketFactory, trustManager) // 可选:加证书公钥锁定,进一步降低中间人风险 val certPinner = CertificatePinner.Builder() // 替换为你的服务IP+端口,以及证书对应的SHA256公钥指纹 .add("xxx.xxx.xxx.xxx:端口号", "sha256/证书SHA256指纹") .build() certificatePinner(certPinner) // 不要写hostnameVerifier { _,_ -> true } 这种全局放开校验的代码,证书SAN配置正确的话不需要 } } }
证书SHA256公钥指纹获取:本地终端执行
openssl x509 -in 你的证书文件.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64,输出结果就是需要填入的指纹值。
iOS端实际实现(iosMain)
iOS端用Darwin引擎,自定义URLSession挑战处理逻辑,把内置证书设为唯一信任锚点:
// iosMain 路径下的SslConfig.kt import io.ktor.client.* import io.ktor.client.engine.darwin.* import platform.Foundation.* import platform.Security.* actual fun HttpClientConfig<*>.configureSelfSignedCertTrust() { engine { // 从App Bundle加载内置证书 val certPath = NSBundle.mainBundle.pathForResource("self_signed_cert", ofType = "crt") ?: throw IllegalArgumentException("自签名证书未找到,请检查iOS工程资源配置") val certData = NSData.dataWithContentsOfFile(certPath) ?: throw IllegalArgumentException("自签名证书读取失败") val anchorCert = SecCertificateCreateWithData(null, certData) ?: throw IllegalArgumentException("证书格式解析失败") handleChallenge { _, challenge, completionHandler -> val protectionSpace = challenge.protectionSpace if (protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) { val serverTrust = protectionSpace.serverTrust!! // 将内置证书设为唯一可信锚点,不使用系统内置信任根证书 SecTrustSetAnchorCertificates(serverTrust, arrayOf(anchorCert)) SecTrustSetAnchorCertificatesOnly(serverTrust, true) var evalError: CFErrorRef? = null val trustResult = SecTrustEvaluateWithError(serverTrust, evalError?.ptr) if (trustResult) { completionHandler( NSURLSessionAuthChallengeUseCredential, NSURLCredential.credentialForTrust(serverTrust) ) } else { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, null) } } else { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, null) } } } }
避坑说明
- 自签证书必须配置SAN字段包含你访问用的静态IP,生成证书时openssl配置里要加
subjectAltName = IP:你的服务端静态IP,否则两端都会报证书身份不匹配错误。 - 后续如果服务端更换证书,必须同步更新客户端内置的证书文件,否则会出现SSL握手失败,建议提前做证书预下发逻辑降低发版依赖。
- 禁止使用信任所有证书、跳过所有主机名校验的实现,就算是内部服务也存在被抓包泄露数据的风险。
内容的提问来源于stack exchange,提问作者Denis Steinman
相关产品推荐
相关产品推荐

