[feat] Dynamic certificate authentication
This commit is contained in:
@@ -19,6 +19,7 @@ import java.net.InetAddress
|
|||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.CertificateException
|
||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
@@ -47,26 +48,67 @@ object ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate): OkHttpClient {
|
fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate?): OkHttpClient {
|
||||||
try {
|
try {
|
||||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
|
val defaultTmFactory = TrustManagerFactory.getInstance(
|
||||||
load(null, null)
|
TrustManagerFactory.getDefaultAlgorithm()
|
||||||
setCertificateEntry("ca", trustedCert)
|
).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 combinedTm = object : X509TrustManager {
|
||||||
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
|
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||||
init(keyStore)
|
return (defaultTm.acceptedIssuers + (customTm?.acceptedIssuers ?: emptyArray()))
|
||||||
}
|
}
|
||||||
|
|
||||||
val trustManager = tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager
|
override fun checkClientTrusted(chain: Array<X509Certificate>, 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<X509Certificate>, 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 {
|
val sslContext = SSLContext.getInstance("TLS").apply {
|
||||||
init(null, arrayOf(trustManager), null)
|
init(null, arrayOf(combinedTm), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return OkHttpClient.Builder()
|
return OkHttpClient.Builder()
|
||||||
.sslSocketFactory(sslContext.socketFactory, trustManager)
|
.sslSocketFactory(sslContext.socketFactory, combinedTm)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -74,9 +116,9 @@ object ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOkHttp(): OkHttpClient
|
fun createOkHttp(cert: String?): OkHttpClient {
|
||||||
{
|
val trustedCert = cert?.let { loadCertificateFromString(it) }
|
||||||
return createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
|
return createOkHttpClientWithDynamicCert(trustedCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRetrofit(): Retrofit {
|
private fun createRetrofit(): Retrofit {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
|||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -70,7 +71,7 @@ fun VariableGrid(
|
|||||||
|
|
||||||
Layout(
|
Layout(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.verticalScroll(scrollState), // ✅ 支持垂直滚动
|
.verticalScroll(scrollState),
|
||||||
content = content
|
content = content
|
||||||
) { measurables, constraints ->
|
) { measurables, constraints ->
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@ fun ComicScreen(
|
|||||||
) {
|
) {
|
||||||
comicScreenViewModel.SetupClient()
|
comicScreenViewModel.SetupClient()
|
||||||
val included = comicScreenViewModel.included
|
val included = comicScreenViewModel.included
|
||||||
|
val state = rememberLazyGridState()
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
|
||||||
@@ -185,7 +187,8 @@ fun ComicScreen(
|
|||||||
columns = GridCells.Adaptive(128.dp),
|
columns = GridCells.Adaptive(128.dp),
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
state = state
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
items(comicScreenViewModel.comics.filter { x ->
|
items(comicScreenViewModel.comics.filter { x ->
|
||||||
|
|||||||
Reference in New Issue
Block a user