From b48f8ce6b03654da920f596ea77d946520284f14 Mon Sep 17 00:00:00 2001 From: acite <1498045907@qq.com> Date: Wed, 10 Sep 2025 14:51:07 +0800 Subject: [PATCH] [feat] Dynamic certificate authentication --- .../acitelight/aether/service/ApiClient.kt | 70 +++++++++++++++---- .../com/acitelight/aether/view/ComicScreen.kt | 7 +- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/acitelight/aether/service/ApiClient.kt b/app/src/main/java/com/acitelight/aether/service/ApiClient.kt index 782c7c8..122fc4b 100644 --- a/app/src/main/java/com/acitelight/aether/service/ApiClient.kt +++ b/app/src/main/java/com/acitelight/aether/service/ApiClient.kt @@ -19,6 +19,7 @@ import java.net.InetAddress import java.net.URL import java.security.KeyStore import java.security.cert.Certificate +import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.net.ssl.SSLContext @@ -47,26 +48,67 @@ object ApiClient { } } - fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate): OkHttpClient { + fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate?): OkHttpClient { try { - val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { - load(null, null) - setCertificateEntry("ca", trustedCert) + val defaultTmFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ).apply { + init(null as KeyStore?) + } + val defaultTm = defaultTmFactory.trustManagers + .first { it is X509TrustManager } as X509TrustManager + + val customTm: X509TrustManager? = trustedCert?.let { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { + load(null, null) + setCertificateEntry("ca", it) + } + val tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ).apply { + init(keyStore) + } + tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager } - val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm() - val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply { - init(keyStore) - } + val combinedTm = object : X509TrustManager { + override fun getAcceptedIssuers(): Array { + return (defaultTm.acceptedIssuers + (customTm?.acceptedIssuers ?: emptyArray())) + } - val trustManager = tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager + override fun checkClientTrusted(chain: Array, authType: String) { + var passed = false + try { + defaultTm.checkClientTrusted(chain, authType) + passed = true + } catch (_: CertificateException) { } + if (!passed && customTm != null) { + customTm.checkClientTrusted(chain, authType) + passed = true + } + if (!passed) throw CertificateException("Untrusted client certificate chain") + } + + override fun checkServerTrusted(chain: Array, authType: String) { + var passed = false + try { + defaultTm.checkServerTrusted(chain, authType) + passed = true + } catch (_: CertificateException) { } + if (!passed && customTm != null) { + customTm.checkServerTrusted(chain, authType) + passed = true + } + if (!passed) throw CertificateException("Untrusted server certificate chain") + } + } val sslContext = SSLContext.getInstance("TLS").apply { - init(null, arrayOf(trustManager), null) + init(null, arrayOf(combinedTm), null) } return OkHttpClient.Builder() - .sslSocketFactory(sslContext.socketFactory, trustManager) + .sslSocketFactory(sslContext.socketFactory, combinedTm) .build() } catch (e: Exception) { @@ -74,9 +116,9 @@ object ApiClient { } } - fun createOkHttp(): OkHttpClient - { - return createOkHttpClientWithDynamicCert(loadCertificateFromString(cert)) + fun createOkHttp(cert: String?): OkHttpClient { + val trustedCert = cert?.let { loadCertificateFromString(it) } + return createOkHttpClientWithDynamicCert(trustedCert) } private fun createRetrofit(): Retrofit { diff --git a/app/src/main/java/com/acitelight/aether/view/ComicScreen.kt b/app/src/main/java/com/acitelight/aether/view/ComicScreen.kt index bc4eeb5..df76056 100644 --- a/app/src/main/java/com/acitelight/aether/view/ComicScreen.kt +++ b/app/src/main/java/com/acitelight/aether/view/ComicScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -70,7 +71,7 @@ fun VariableGrid( Layout( modifier = modifier - .verticalScroll(scrollState), // ✅ 支持垂直滚动 + .verticalScroll(scrollState), content = content ) { measurables, constraints -> @@ -137,6 +138,7 @@ fun ComicScreen( ) { comicScreenViewModel.SetupClient() val included = comicScreenViewModel.included + val state = rememberLazyGridState() Column { @@ -185,7 +187,8 @@ fun ComicScreen( columns = GridCells.Adaptive(128.dp), contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), + state = state ) { items(comicScreenViewModel.comics.filter { x ->