You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose中基于自定义SSL Socket Factory实现无第三方库的SSL公钥固定?

在Jetpack Compose中实现公钥固定的SSL Pinning(无第三方库)

公钥固定(SSL Pinning)的核心是将服务器的公钥哈希硬编码到应用中,在建立SSL连接时验证服务器返回的证书链中是否包含匹配的公钥,以此防止中间人攻击。以下是完全基于Android原生API、自定义SSLSocketFactory的实现方案:

1. 获取服务器公钥的SHA-256哈希值

首先需要提取目标服务器公钥的Base64编码SHA-256哈希,可通过OpenSSL命令行获取:

openssl s_client -connect your-server-domain.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

将输出的字符串保存下来,作为后续验证的可信哈希值(建议保留多个备用哈希,应对服务器证书更新)。

2. 实现自定义X509TrustManager

自定义信任管理器是验证公钥的核心,它会在SSL握手时检查服务器公钥是否匹配预设的哈希:

import java.security.MessageDigest
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager

class PinningTrustManager(private val allowedPublicKeyHashes: List<String>) : X509TrustManager {

    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
        // 若无需验证客户端证书,可空实现;有需求时在此添加客户端证书校验逻辑
    }

    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
        chain ?: throw IllegalArgumentException("Certificate chain cannot be null")

        // 遍历证书链,提取每个证书的公钥并计算哈希
        for (certificate in chain) {
            val publicKeyBytes = certificate.publicKey.encoded
            val sha256Digest = MessageDigest.getInstance("SHA-256").digest(publicKeyBytes)
            val base64Hash = android.util.Base64.encodeToString(sha256Digest, android.util.Base64.NO_WRAP)

            // 匹配到可信哈希则验证通过
            if (allowedPublicKeyHashes.contains(base64Hash)) {
                return
            }
        }

        // 无匹配公钥时抛出安全异常,终止连接
        throw SecurityException("Server public key pinning failed: no trusted public key found")
    }

    override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}

3. 构建带Pinning的OkHttpClient

通过自定义的PinningTrustManager创建SSLSocketFactory,并封装到OkHttpClientFactory中供全局使用:

import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import okhttp3.OkHttpClient

object OkHttpClientFactory {

    // 替换为你获取到的可信公钥哈希,支持多个哈希值
    private val TRUSTED_PUBLIC_KEY_HASHES = listOf(
        "YOUR_TRUSTED_PUBLIC_KEY_HASH_1",
        "YOUR_TRUSTED_PUBLIC_KEY_HASH_2"
    )

    fun createPinningOkHttpClient(): OkHttpClient {
        val trustManager = PinningTrustManager(TRUSTED_PUBLIC_KEY_HASHES)

        // 初始化TLS上下文,绑定自定义信任管理器
        val sslContext = SSLContext.getInstance("TLS")
        sslContext.init(null, arrayOf<TrustManager>(trustManager), java.security.SecureRandom())
        val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory

        return OkHttpClient.Builder()
            .sslSocketFactory(sslSocketFactory, trustManager)
            // 保留默认主机名验证(无需自定义,除非有特殊业务需求)
            .build()
    }
}

4. 在Jetpack Compose中集成使用

将带Pinning的OkHttpClient与Retrofit(或其他网络库)结合,在Compose界面中发起网络请求:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

// 定义API接口
interface ApiService {
    @GET("your-api-endpoint")
    suspend fun fetchData(): YourResponseModel
}

// 全局Retrofit实例(建议使用DI框架管理,如Hilt)
private val retrofit = Retrofit.Builder()
    .baseUrl("https://your-server-domain.com/")
    .client(OkHttpClientFactory.createPinningOkHttpClient())
    .addConverterFactory(GsonConverterFactory.create())
    .build()

private val apiService = retrofit.create(ApiService::class.java)

@Composable
fun NetworkRequestScreen() {
    val responseText = remember { mutableStateOf<String?>(null) }
    val errorText = remember { mutableStateOf<String?>(null) }

    Column {
        Button(onClick = {
            LaunchedEffect(Unit) {
                try {
                    val response = apiService.fetchData()
                    responseText.value = "请求成功:${response.toString()}"
                } catch (e: Exception) {
                    errorText.value = "请求失败:${e.message}"
                }
            }
        }) {
            Text("获取数据")
        }

        responseText.value?.let { Text(it) }
        errorText.value?.let { Text(it) }
    }
}

关键注意事项

  • 多哈希预留:服务器更新证书时,新的公钥哈希需提前加入TRUSTED_PUBLIC_KEY_HASHES列表,避免应用突然无法连接。
  • 验证测试:修改哈希值为无效值,确认连接时会抛出SecurityException,确保固定逻辑生效。
  • 客户端证书:若服务器要求验证客户端证书,在checkClientTrusted方法中添加相应校验逻辑。
  • TLS版本:确保SSLContext使用TLS 1.2及以上版本,提升安全性。

内容的提问来源于stack exchange,提问作者Nachiket Gohil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 12:15:55