如何在含原生模块与WebView/Cordova的APP中白名单自签名SSL证书?
针对你的混合架构移动应用(原生模块 + WebView/Cordova 嵌入的HTML5),要让它顺利信任你那套已经在Web应用里用起来的自签名SSL证书,得分开处理原生HTTPS调用和WebView内的AJAX请求,下面是具体的实操方案:
原生部分的HTTPS请求得让APP本身信任自签名证书,分iOS和Android两个平台单独处理:
iOS平台
方案1:内置证书到APP(推荐,无需用户手动操作)
把你的自签名证书(.cer或.pem格式)拖进Xcode项目,记得勾选目标APP,确保它出现在Build Phases的Copy Bundle Resources列表里。然后自定义URLSession的信任策略,验证服务器返回的证书和我们内置的是否匹配:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard let serverTrust = challenge.protectionSpace.serverTrust else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 加载APP里的自签名证书 guard let certPath = Bundle.main.path(forResource: "your-custom-cert", ofType: "cer"), let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)), let targetCert = SecCertificateCreateWithData(nil, certData as CFData) else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 把我们的证书设为信任锚点 let trustedCerts = [targetCert] SecTrustSetAnchorCertificates(serverTrust, trustedCerts as CFArray) // 验证服务器证书 var trustResult: SecTrustResultType = .invalid SecTrustEvaluate(serverTrust, &trustResult) if trustResult == .unspecified || trustResult == .proceed { // 证书匹配,允许连接 let credential = URLCredential(trust: serverTrust) completionHandler(.useCredential, credential) } else { // 证书不匹配,拒绝连接 completionHandler(.cancelAuthenticationChallenge, nil) } }
方案2:配置ATS例外(辅助用,适配旧版iOS)
如果是iOS13以下的版本,可能需要在Info.plist里添加ATS配置,给后端域名开例外:
<key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>your-backend-domain.com</key> <dict> <key>NSExceptionRequiresForwardSecrecy</key> <false/> <key>NSIncludesSubdomains</key> <true/> </dict> </dict> </dict>
Android平台
方案1:内置证书到APP(推荐)
先把自签名证书转成Android支持的BKS格式(需要BouncyCastle工具):
keytool -importcert -file your-cert.crt -keystore your-cert.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
把生成的your-cert.bks放到res/raw目录下,然后自定义TrustManager让OkHttp(或HttpURLConnection)信任这个证书:
private SSLSocketFactory getCustomSSLSocketFactory(Context context) throws Exception { // 加载内置证书 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); InputStream certStream = context.getResources().openRawResource(R.raw.your_cert); Certificate cert = certFactory.generateCertificate(certStream); certStream.close(); // 初始化密钥库并添加证书 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry("trusted-cert", cert); // 创建信任管理器 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); // 构建SSL上下文 SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null); return sslContext.getSocketFactory(); }
然后给OkHttpClient配置这个自定义的SSLSocketFactory:
OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(getCustomSSLSocketFactory(context), (X509TrustManager) tmf.getTrustManagers()[0]) .build();
方案2:让用户手动安装证书(适合非企业应用)
如果不想内置证书,可以指导用户把自签名证书安装到Android系统的「加密与凭据」里(路径大概是:设置 → 安全 → 加密与凭据 → 从存储安装),这样原生HTTPS调用会自动信任该证书。
这部分的处理分Cordova插件和原生WebView两种场景:
Cordova环境
推荐:使用cordova-plugin-advanced-http插件
这个插件专门支持自定义证书信任,把自签名证书放到www/assets目录下,然后初始化时指定证书:
const http = cordova.plugin.http; // 设置证书锁定模式 http.setServerTrustMode('pinned', () => { console.log('信任模式已设置为证书锁定'); }, (err) => { console.error('设置信任模式失败', err); }); // 或者直接指定证书文件 http.setSSLCertMode('your-cert.crt', () => { console.log('SSL证书模式已配置'); }, (err) => { console.error('配置SSL证书模式失败', err); });
测试环境临时方案(生产禁用)
如果只是测试,可以在config.xml里加配置允许访问目标域名,但生产环境绝对不能这么做:
<access origin="https://your-backend-domain.com" /> <allow-navigation href="https://your-backend-domain.com/*" />
原生WebView(非Cordova)
Android WebView
自定义WebViewClient,重写onReceivedSslError方法,一定要验证证书再允许加载,别直接忽略所有错误(会被中间人攻击):
webView.setWebViewClient(new WebViewClient() { @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { // 这里要对比服务器返回的证书和我们内置的证书是否一致 if (isOurCustomCert(error.getCertificate())) { handler.proceed(); // 证书匹配,允许加载 } else { handler.cancel(); // 证书不匹配,拒绝 } } }); // 别忘了启用JS和DOM存储,不然AJAX会有问题 WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setDomStorageEnabled(true);
iOS WKWebView
自定义WKNavigationDelegate,在证书挑战时做验证,逻辑和原生URLSession的处理一致:
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { // 这里复用之前原生模块的证书验证逻辑,确认是我们的自签名证书再允许连接 // 验证通过就调用completionHandler(.useCredential, credential),否则取消 }
- 安全性第一:绝对不要直接跳过SSL错误验证(比如Android直接
handler.proceed()、iOS直接允许所有挑战),这等于给中间人攻击开了后门,必须严格验证证书的有效性。 - 证书更新:如果自签名证书过期或更换,要么重新打包APP更新内置证书,要么指导用户重新安装系统证书。
- 环境区分:测试环境可以临时放宽限制,但生产环境必须严格遵循证书验证流程。
内容的提问来源于stack exchange,提问作者hellopeach

