配置 https 证书,如果证书过期,希望不影响访问,这个怎么做。 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
zhixiz1007
V2EX    Android

配置 https 证书,如果证书过期,希望不影响访问,这个怎么做。

  •  
  •   zhixiz1007 19 小时 29 分钟前 2868 次点击

    帮公司的安卓小哥来提个问题:之前项目是没有配置证书的,可以被抓包,现在为了防止抓包,就需要配置 https 证书。首先静态配置证书是属于写到 network_security_config_app.xml 这个证书,直接配置到网络认证文件里,这个种情况有个不好就是如果哪天证书过期了,那就无法通过网络认证了,但是不希望如此,希望如果过期了那就忽略认证,所以需要动态配置 ssl 不直接配置到文件中,通过代码去认证,先去判断证书有效,如果无或者不能找到证书直接走信任证书,如果有效那就走证书认证,目前没配置的表现就是可以被抓包。请问大家,有过这方面的经验吗? GPT 代码帮忙解决了,但是因为是知识盲区想请教一下诸位。 package com.wlld.common.network

    import android.content.Context import android.util.Log import com.wlld.common.utils.LogUtils import okhttp3.OkHttpClient import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager import java.security.SecureRandom

    /**

    • OkHttp SSL 配置工具类

    • 提供动态证书校验支持 */ object SSLConfigUtil {

      private const val TAG = "SSLConfigUtil" private var dynamicTrustManager: DynamicSSLTrustManager? = null

      /**

      • 配置 OkHttp 使用动态 SSL 证书校验 */ fun OkHttpClient.Builder.configureDynamicSSL(context: Context): OkHttpClient.Builder { LogUtils.d(TAG, " 开始配置动态 SSL...") return try { // 创建动态信任管理器 dynamicTrustManager = DynamicSSLTrustManager.create(context) LogUtils.d(TAG, " 动态信任管理器创建成功")

         // 创建 SSL 上下文 val sslCOntext= SSLContext.getInstance("TLS") sslContext.init(null, arrayOf<TrustManager>(dynamicTrustManager!!), SecureRandom()) LogUtils.d(TAG, " SSL 上下文初始化完成") // 配置 SSL Socket Factory sslSocketFactory(sslContext.socketFactory, dynamicTrustManager!!) LogUtils.d(TAG, " SSL Socket Factory 配置完成") LogUtils.d(TAG, " 动态 SSL 配置应用成功!") this 

        } catch (e: Exception) { Log.e(TAG, " 动态 SSL 配置失败", e) // 降级到不安全的配置(仅用于紧急情况) configureUnsafeSSL() } }

      /**

      • 配置不安全的 SSL (信任所有证书)

      • 仅在紧急情况下使用 */ private fun OkHttpClient.Builder.configureUnsafeSSL(): OkHttpClient.Builder { Log.w(TAG, "Using unsafe SSL configuration - trusts all certificates") return try { val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager { override fun checkClientTrusted(chain: Array<out java.security.cert.X509Certificate>, authType: String) {} override fun checkServerTrusted(chain: Array<out java.security.cert.X509Certificate>, authType: String) {} override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf() })

         val sslCOntext= SSLContext.getInstance("SSL") sslContext.init(null, trustAllCerts, SecureRandom()) sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) hostnameVerifier { _, _ -> true } this 

        } catch (e: Exception) { Log.e(TAG, "Failed to configure unsafe SSL", e) this } }

      /**

      • 动态添加证书 */ fun addCertificate(certificate: java.security.cert.X509Certificate) { dynamicTrustManager?.addCertificate(certificate) }

      /**

      • 从字节数组添加证书 */ fun addCertificateFromBytes(certificateBytes: ByteArray) { dynamicTrustManager?.addCertificateFromBytes(certificateBytes) }

      /**

      • 设置是否跳过证书校验 */ fun setSkipValidation(skip: Boolean) { dynamicTrustManager?.setSkipValidation(skip) }

      /**

      • 检查是否跳过校验模式 */ fun isSkipValidationEnabled(): Boolean { return dynamicTrustManager?.isSkipValidationEnabled() ?: false }

      /**

      • 获取证书信息 */ fun getCertificateInfo(): List<String> { return dynamicTrustManager?.getCertificateInfo() ?: emptyList() }

      /**

      • 检查证书是否过期 */ fun isCertificateExpired(certificate: java.security.cert.X509Certificate): Boolean { return DynamicSSLTrustManager.isCertificateExpired(certificate) }

      /**

      • 重置 SSL 配置 */ fun reset() { dynamicTrustManager = null LogUtils.d(TAG, "SSL configuration reset") } }

    package com.wlld.common.network

    import android.content.Context import android.util.Log import com.wlld.common.R import java.io.ByteArrayInputStream import java.io.InputStream import java.security.KeyStore import java.security.KeyStoreException import java.security.NoSuchAlgorithmException import java.security.PrivateKey import java.security.cert.Certificate import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.Date import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager

    /**

    • 动态 HTTPS 证书校验工具类

    • 当证书过期时跳过校验,确保应用正常运行 */ class DynamicSSLTrustManager( private val context: Context ) : X509TrustManager {

      private val defaultTrustManager: X509TrustManager private val customCertificates: MutableList<X509Certificate> = mutableListOf() private var skipValidation = false

      companion object { private const val TAG = "DynamicSSLTrustManager"

       // 检查证书是否过期 fun isCertificateExpired(certificate: X509Certificate): Boolean { return try { val expiratiOnDate= certificate.notAfter expirationDate.before(Date()) } catch (e: Exception) { Log.e(TAG, "证书过期检查出错", e) true // 如果无法检查,默认认为过期 } } // 创建动态信任管理器 fun create(context: Context): DynamicSSLTrustManager { return DynamicSSLTrustManager(context) } 

      }

      init { Log.d(TAG, " DynamicSSLTrustManager 初始化开始")

       // 初始化默认信任管理器 defaultTrustManager = createDefaultTrustManager() Log.d(TAG, " 默认信任管理器初始化完成") // 加载自定义证书 loadCustomCertificates() Log.d(TAG, " DynamicSSLTrustManager 初始化完成 - 跳过校验模式: $skipValidation, 自定义证书数量: ${customCertificates.size}") 

      }

      private fun createDefaultTrustManager(): X509TrustManager { return try { val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustManagerFactory.init(null as KeyStore?) val trustManagers = trustManagerFactory.trustManagers trustManagers.firstOrNull { it is X509TrustManager } as? X509TrustManager ?: throw IllegalStateException("Default X509TrustManager not found") } catch (e: Exception) { Log.e(TAG, "Failed to create default trust manager", e) throw RuntimeException("Failed to initialize SSL trust manager", e) } }

      private fun loadCustomCertificates() { try { // 加载 assets 中的证书文件 loadCertificateFromAssets("weilaiqiyuan.cer")

       // 可以继续加载其他证书 // loadCertificateFromAssets("other_certificate.cer") Log.d(TAG, "已加载 ${customCertificates.size} 个自定义证书") // 如果没有找到任何证书,启用跳过校验模式 if (customCertificates.isEmpty()) { Log.w(TAG, "未找到任何自定义证书,启用跳过校验模式") skipValidation = true } } catch (e: Exception) { Log.e(TAG, "加载自定义证书失败,启用跳过校验模式", e) skipValidation = true } 

      }

      private fun loadCertificateFromAssets(fileName: String) { try { context.assets.open(fileName).use { inputStream -> val certificate = loadCertificateFromStream(inputStream) if (certificate != null) { customCertificates.add(certificate) Log.d(TAG, "已加载证书: $fileName, 过期时间: ${certificate.notAfter}")

       // 检查证书是否过期 if (isCertificateExpired(certificate)) { Log.w(TAG, "证书 $fileName 已过期,启用跳过校验模式") skipValidation = true } } } } catch (e: Exception) { Log.e(TAG, "从 assets 加载证书失败: $fileName", e) } 

      }

      private fun loadCertificateFromStream(inputStream: InputStream): X509Certificate? { return try { val certificateFactory = CertificateFactory.getInstance("X.509") certificateFactory.generateCertificate(inputStream) as X509Certificate } catch (e: Exception) { Log.e(TAG, "解析证书失败", e) null } }

      /**

      • 动态添加证书 */ fun addCertificate(certificate: X509Certificate) { customCertificates.add(certificate) Log.d(TAG, "已添加新证书, 过期时间: ${certificate.notAfter}")

        // 检查新添加的证书是否过期 if (isCertificateExpired(certificate)) { Log.w(TAG, "新添加的证书已过期,启用跳过校验模式") skipValidation = true } }

      /**

      • 动态添加证书从字节数组 */ fun addCertificateFromBytes(certificateBytes: ByteArray) { try { val inputStream = ByteArrayInputStream(certificateBytes) val certificate = loadCertificateFromStream(inputStream) certificate?.let { addCertificate(it) } } catch (e: Exception) { Log.e(TAG, "从字节数组添加证书失败", e) } }

      /**

      • 设置是否跳过校验 */ fun setSkipValidation(skip: Boolean) { skipValidation = skip Log.i(TAG, "跳过校验模式: $skip") }

      /**

        跳过校验模式是否开启 */ fun isSkipValidationEnabled(): Boolean { return skipValidation }

      /**

      • 检查证书链中的所有证书是否有效 */ private fun checkCertificatesValidity(chain: Array<X509Certificate>): Boolean { return chain.all { certificate -> val isValid = !isCertificateExpired(certificate) if (!isValid) { Log.w(TAG, "证书已过期: ${certificate.subjectDN}, 过期时间: ${certificate.notAfter}") } isValid } }

      override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) { // if (skipValidation) { // Log.w(TAG, " 跳过客户端证书校验(证书过期或未找到)- 这可能导致抓包风险!") // return // } // // // 先检查自定义证书 // if (validateCustomCertificates(chain)) { // Log.d(TAG, "使用有效的自定义证书") // return // } // // // 使用默认校验 // try { // defaultTrustManager.checkClientTrusted(chain, authType) // } catch (e: CertificateException) { // Log.w(TAG, "默认客户端证书校验失败", e) // // // 检查是否因为过期导致的失败 // if (!checkCertificatesValidity(chain as Array<X509Certificate>)) { // Log.w(TAG, "客户端证书已过期,启用跳过校验模式") // skipValidation = true // return // } // // throw e // } }

      override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) { Log.d(TAG, " checkServerTrusted 被调用 - 验证服务器证书,authType: $authType") if (skipValidation) { Log.i(TAG, "跳过服务端证书校验(证书过期或未找到)") return }

       // 先检查自定义证书 if (validateCustomCertificates(chain)) { return } // 使用默认校验 try { defaultTrustManager.checkServerTrusted(chain, authType) } catch (e: CertificateException) { Log.w(TAG, "默认服务端证书校验失败", e) // 检查是否因为过期导致的失败 if (!checkCertificatesValidity(chain as Array<X509Certificate>)) { Log.w(TAG, "服务端证书已过期,启用跳过校验模式") skipValidation = true return } throw e } 

      }

      override fun getAcceptedIssuers(): Array<X509Certificate> { return if (skipValidation) { // 跳过校验时返回空数组,接受所有证书 emptyArray() } else { // 返回默认接受的证书颁发机构 defaultTrustManager.acceptedIssuers + customCertificates.toTypedArray() } }

      /**

      • 验证自定义证书链 */ private fun validateCustomCertificates(chain: Array<out X509Certificate>): Boolean { if (customCertificates.isEmpty()) { return false }

    // // 如果自定义证书有效,优先使用 // for (customCert in customCertificates) { // if (!isCertificateExpired(customCert)) { // // 检查自定义证书是否覆盖 weilaiqiyuan.com 域名 // val subjectCN = extractCommonName(customCert.subjectDN.toString()) // if (subjectCN.contains("*.weilaiqiyuan.com") || subjectCN.contains("weilaiqiyuan.com")) { // Log.d(TAG, "使用有效的自定义证书验证 weilaiqiyuan.com 域名: $subjectCN") // return true // } // } // }

     // 检查链中的证书是否与自定义证书匹配 for (cert in chain) { for (customCert in customCertificates) { if (cert.subjectDN == customCert.subjectDN) { // 找到匹配的自定义证书 if (!isCertificateExpired(customCert)) { // 严格验证证书:比较公钥指纹,防止证书伪造 if (verifyCertificateFingerprint(cert, customCert)) { Log.d(TAG, " 证书验证通过(指纹匹配): ${cert.subjectDN}") return true } else { Log.w(TAG, " 证书验证失败(指纹不匹配)- 可能是抓包攻击!") // 指纹不匹配,拒绝连接 throw CertificateException("证书指纹验证失败,可能存在中间人攻击") } } else { Log.w(TAG, "自定义证书已过期: ${cert.subjectDN}") return false } } } } return false } /** * 验证证书指纹,防止证书伪造 */ private fun verifyCertificateFingerprint(cert1: X509Certificate, cert2: X509Certificate): Boolean { return try { // 比较公钥的 SHA-256 指纹 val pubkey1 = cert1.publicKey.encoded val pubkey2 = cert2.publicKey.encoded val digest1 = java.security.MessageDigest.getInstance("SHA-256").digest(pubkey1) val digest2 = java.security.MessageDigest.getInstance("SHA-256").digest(pubkey2) val fingerprint1 = digest1.joinToString("") { "%02X".format(it) } val fingerprint2 = digest2.joinToString("") { "%02X".format(it) } val isValid = fingerprint1 == fingerprint2 Log.d(TAG, "证书指纹对比: $fingerprint1 vs $fingerprint2, 匹配: $isValid") isValid } catch (e: Exception) { Log.e(TAG, "证书指纹验证失败", e) false } } /** * 从 SubjectDN 中提取 Common Name */ private fun extractCommonName(subjectDN: String): String { val cnPattern = "CN=([^,]+)".toRegex() val match = cnPattern.find(subjectDN) return match?.groupValues?.get(1) ?: "" } /** * 获取自定义证书信息 */ fun getCertificateInfo(): List<String> { return customCertificates.map { cert -> "Subject: ${cert.subjectDN}, Issuer: ${cert.issuerDN}, Expires: ${cert.notAfter}, Expired: ${isCertificateExpired(cert)}" } } 

    }

    19 条回复    2025-10-11 10:27:13 +08:00
    samIIsun
        1
    samIIsun  
       19 小时 24 分钟前
    看得出目的主要是:"为了防止抓包"
    buuuut ,用 https 也可以中间人抓包。。。我抓安卓的 http 包,不 care 服务端有没有 https 的。。。
    unused
        2
    unused  
       19 小时 22 分钟前
    自签
    gesse
        3
    gesse  
       19 小时 14 分钟前
    https/tls 库都有 insecure 忽略证书有效性、不验证证书日期有效性等选项吧。
    Bananana
        4
    Bananana  
       19 小时 9 分钟前
    客户端验证公钥?这样证书过期了,只要重签时候公钥不变就不会报错
    sujin190
        5
    sujin190  
       19 小时 6 分钟前
    不需要这么费劲吧,常用的库比如 okhttp 都支持设置自定义证书验证回调吧,在回调里处理下直接忽略证书过期然后改成直接验证服务器证书指纹就好了啊,也是安全的

    import okhttp3.*;
    import javax.net.ssl.*;
    import java.security.cert.X509Certificate;

    public class CustomVerifyOkHttp {

    public static OkHttpClient buildClient() throws Exception {
    TrustManager[] trustManagers = new TrustManager[]{
    new X509TrustManager() {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) {
    // 不处理客户端
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
    throws CertificateException {
    // 示例:只信任特定公钥指纹
    X509Certificate cert = chain[0];
    String sha256 = sha256(cert.getPublicKey().getEncoded());
    if (!"your_expected_sha256_fingerprint".equalsIgnoreCase(sha256)) {
    throw new CertificateException("Untrusted server certificate");
    }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
    return new X509Certificate[0];
    }
    }
    };

    SSLContext sslCOntext= SSLContext.getInstance("TLS");
    sslContext.init(null, trustManagers, new java.security.SecureRandom());

    return new OkHttpClient.Builder()
    .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0])
    .hostnameVerifier((hostname, session) -> true) // 可选:略过主机名验证
    .build();
    }

    private static String sha256(byte[] data) throws Exception {
    java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
    byte[] digest = md.digest(data);
    StringBuilder sb = new StringBuilder();
    for (byte b : digest) sb.append(String.format("%02x", b));
    return sb.toString();
    }
    }


    基本就是这样,这样之后楼上说的什么中间人抓包也没可能了
    miyuki
        6
    miyuki  
       19 小时 4 分钟前
    请求库一般可以设置忽略“不安全”证书

    提高抓包难度还能验证服务端证书,防止被中间人( ssl pinning )
    iyaozhen
        7
    iyaozhen  
       18 小时 27 分钟前
    Android 很好解决吧。现在非 root 几乎不能抓包了
    你可以设置不信任用户导入的证书,只信任系统的。大概是这个意思。

    我是 QA ,每次要抓自己 app 的包,都需要研发单独打 dev 的开发包,去掉这个限制
    Esec
        8
    Esec  
       16 小时 51 分钟前 via Android
    然后就开始头疼证书注入了..
    busier
        9
    busier  
       15 小时 24 分钟前 via iPhone
    1 、服务器不要求双向证书验证

    2 、客户端忽略证书错误
    czyt
        10
    czyt  
       13 小时 45 分钟前
    caddy 貌似自动申请的
    mmdsun
        11
    mmdsun  
       13 小时 42 分钟前
    自签一个 9999 年到期的证书。
    edsion1107
        12
    edsion1107  
       12 小时 18 分钟前
    留个后门,把 https 降级到 http 。
    只要是 https 都走你自己的证书信任链,过期、第三方证书都按非法证书处理。

    感觉你的思路是在把问题复杂化,而且好像也有点违背 https 设计的初衷。
    imlonghao
        13
    imlonghao  
       12 小时 16 分钟前
    及时更新证书别让他过期
    clarkethan
        14
    clarkethan  
       10 小时 17 分钟前
    我们是通过限制签发 CA 来做的,不允许未知 CA 签发的证书,因为会通过固定的 CA 签发证书,所以这个策略比较有效
    ysc3839
        15
    ysc3839  
       7 小时 44 分钟前 via Android
    客户端好办,自己生成超长时间证书然后信任即可
    cctv6
        16
    cctv6  
       4 小时 7 分钟前 via Android   1
    op 应该需要的就是 SSL Pinning ,已经是很成熟的方案了。

    实现这个的方式有很多种,固定域名证书只是方法之一,但是也有弊端,也就是 op 说的证书会过期。
    如果想不过期,那可以用自签。

    你也可以校验证书链上的根证书或者中间证书,如果是根证书,通常有效期是几十年。

    客户端要做的就是验证服务器证书链是不是有效的。中间人在攻击的时候总是会使用其他的 CA 签发证书,如果你在 ssl 连接握手阶段去校验证书是不是你的 CA 签发的,校验通过,说明没有中间人,校验不通过,说明有中间人,然后中断连接就可以了。
    wen20
        17
    wen20  
       4 小时 5 分钟前
    不是浏览器中访问就自签呗
    virusdefender
        18
    virusdefender  
       1 小时 48 分钟前
    自签名就够了
    COW
        19
    COW  
       1 小时 10 分钟前
    自签名 SSL Pinning ,公钥指纹固定,注意下指纹内容存储位置 Keystore/Keychain ,过期了更新指纹即可。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5870 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 55ms UTC 03:37 PVG 11:37 LAX 20:37 JFK 23:37
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86