Android 解决定制设备 SSL 证书不信任:OkHttp+Glide 全局适配

记录一次老项目维护排坑过程,定制平板设备因系统根证书缺失,出现接口请求白屏、部分图片加载失败问题。分别对 OkHttp 接口工具类、Glide 图片加载做 SSL 证书兜底适配,记录问题定位与完整实现代码,方便后续复用。

背景:这是我接手维护的上古代码老项目,一开始业务那边反馈有一个客户的平板设备打开就是空白,显然就是接口加载失败,就很奇怪明明之前也出货过很多的客户的不同机型,也没有这个问题,所以第一反应就怀疑是系统问题。

然后拿到设备开始复现、分析日志。 很明显的找到了

ini 复制代码
请求失败: Unacceptable certificate: CN=AAA Certificate Services

问题定位

1. 证书错误导致关键配置接口失败(直接原因)

这个错误来来自未封装的 OkHttpNetworkUtils (在 HomeFragment.init()loadData() 中使用)。虽然在 NetworkSecurityConfig 里配置了信任用户/系统证书,但 OkHttpNetworkUtils 自己 new 出来的 OkHttpClient 并没有读取这个配置,导致 HTTPS 握手失败。

ApiUtils / HttpUtils(Retrofit 那套)的接口都正常返回,这说明后端接口和证书本身没问题 ,问题只出在客户端 OkHttpNetworkUtils 的 SSL 配置上。

这个项目的大部分接口请求都走已封装好的ApiUtils工具类,然而居然还有一些零散的接口写在各个页面中,还是首页走的半封装的OkHttpNetworkUtil。

此时,我有2个选择: 1、把所有接口统一都走ApiUtils 2、把OkHttpNetworkUtil也配一下证书信任。 我看了一下OkHttpNetworkUtil有十几处地方调用,且由于任务紧急 于是我选择了2,修复的代码如下

java 复制代码
public class OkHttpNetworkUtils {
    public static String TAG = "OkHttpNetworkUtils";

    private static final OkHttpClient client = createClient();

    private static OkHttpClient createClient() {
        try {
            // 1. 加载系统默认 TrustManager(会读取系统证书 + network_security_config.xml 配置)
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            tmf.init((KeyStore) null);

            // 2. 提取系统 X509TrustManager
            final X509TrustManager systemTm = extractX509(tmf.getTrustManagers());

            // 3. 包装一层:系统校验失败时记录日志并放行
            //    解决部分定制 Android 系统缺失 Comodo/Sectigo 等根证书导致的握手失败
            X509TrustManager wrappedTm = new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    if (systemTm != null) {
                        systemTm.checkClientTrusted(chain, authType);
                    }
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    if (systemTm != null) {
                        try {
                            systemTm.checkServerTrusted(chain, authType);
                        } catch (CertificateException e) {
                            // 打印证书信息,方便排查,然后放行
                            Log.w(TAG, "System cert check failed, allowing anyway. " +
                                    "Subject: " + (chain.length > 0 ? chain[0].getSubjectDN() : "unknown") +
                                    ", Error: " + e.getMessage());
                        }
                    }
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return systemTm != null ? systemTm.getAcceptedIssuers() : new X509Certificate[0];
                }
            };

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{wrappedTm}, new java.security.SecureRandom());

            return new OkHttpClient.Builder()
                    .sslSocketFactory(sslContext.getSocketFactory(), wrappedTm)
                    .hostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier())
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .writeTimeout(15, TimeUnit.SECONDS)
                    .build();

        } catch (Exception e) {
            Log.e(TAG, "createClient failed, fallback to default", e);
            return new OkHttpClient.Builder()
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .build();
        }
    }

    private static X509TrustManager extractX509(TrustManager[] tms) {
        for (TrustManager tm : tms) {
            if (tm instanceof X509TrustManager) {
                return (X509TrustManager) tm;
            }
        }
        return null;
    }

    public static void makeGetRequest(String urlString, final Callback callback) {
        Request request = new Request.Builder()
                .url(urlString)
                .build();

        client.newCall(request).enqueue(callback);
    }

    // 示例Callback实现
    public static abstract class SimpleCallback implements Callback {
        @Override
        public void onFailure(Call call, IOException e) {
            onError(e);
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            try {
                if (!response.isSuccessful()) {
                    response.close(); // 防止泄漏
                    onError(new IOException("Unexpected code " + response));
                    return;
                }
                String responseBody = response.body().string();
                onSuccess(responseBody);
            } catch (Exception e) {
                Log.e(TAG, e.getMessage());
            } finally {
                // 确保异常路径下也关闭 response
                if (response != null) {
                    response.close();
                }
            }
        }

        public abstract void onSuccess(String responseBody);

        public abstract void onError(Exception e);
    }
}

总结1:接口请求还是要统一封装和管理,对于 B 端定制 Android 设备,永远假设系统证书库不完整,HTTPS 层必须做兜底设计。

好了,那么为啥以前出货过的其他机器没这个问题呢?

核心原因是 设备内置的「根证书库」不一样 ,以及 Android 版本对证书校验策略的差异

我们的客户大多是B端小厂出的设备,这类机器的系统往往是厂商深度定制的,证书库被裁剪得很厉害。

1. 厂商把系统证书库裁了(最常见)

低端或定制 ROM(平板)为了省空间,会把不常用的根证书从 /system/etc/security/cacerts/ 里删掉。

  • Comodo / Sectigo / AAA Certificate Services 这类证书,大厂手机都有,但小厂低端设备经常缺
  • 之前客户的机器可能内置了这个根证书,现在这批机器没有

2. Android 7.0+ 的证书限制(API 24 分水岭)

从 Android 7.0 开始,Google 默认不再信任用户手动安装的根证书,且对证书链校验更严格。

表格

场景 结果
客户机器是 Android 5/6 可能默认信任用户证书,或者校验宽松,没问题
客户机器是 Android 10/11/14 严格校验 + 可能缺证书,就崩
network_security_config.xml 配置了信任用户证书 这个配置只对 App 自己的 Retrofit/HttpURLConnection 生效OkHttpNetworkUtilsnew OkHttpClient() 默认不读取这个配置

大厂 App(微信、抖音)之所以没问题,是因为它们要么:

  • 内置了完整的证书链(证书钉扎)
  • 做了系统校验失败后的兜底放行

这个设备可能刚好踩到了「系统缺 Comodo 根证书」的坑,所以需要代码层面兜底。

你以为,到此就结束了么?还没有呢! 还有图片的问题

上面的问题解决后,继续观察程序运行, 然后发现有的图片加载不出来,图片加载统一用的GlideUtils,于是我在加载不出来的图片位置加了日志打印和try catch, 然后很快发现我们自己的域名和腾讯域名的图片是正常加载显示,2345域名的图片都加载失败了。日志中发现了关键

Caused by: javax.net.ssl.SSLHandshakeException: Unacceptable certificate: CN=AAA Certificate Services, O=Comodo CA Limited, L=Salford, ST=Greater Manchester, C=GB

是 SSL 证书链问题的问题。

上面的 OkHttpNetworkUtils 配的 SSL 兜底,只覆盖了接口请求,没覆盖 Glide 。Glide 默认用系统裸的 HttpURLConnection,遇到 2345 CDN 的证书链,在缺根证书的设备上直接 SSL 握手失败,所以图片显示空白。

解决方案:让 Glide配 SSL 兜底

Glide 默认不走 OkHttp,必须显式集成 okhttp3-integration,然后「系统校验失败也放行」的 OkHttpClient 注册给 Glide。

1. build.gradle 加依赖

arduino 复制代码
implementation 'com.github.bumptech.glide:glide:4.14.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
// 关键:Glide 的 OkHttp 集成模块
implementation 'com.github.bumptech.glide:okhttp3-integration:4.14.2'

2. 创建 AppGlideModule(SSL 兜底)

java 复制代码
@GlideModule
public class MyAppGlideModule extends AppGlideModule {

    private static final String TAG = "GlideSSL";

    @Override
    public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
        OkHttpClient client = createSafeClient();
        registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));
    }

    private OkHttpClient createSafeClient() {
        try {
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            tmf.init((KeyStore) null);
            final X509TrustManager systemTm = extractX509(tmf.getTrustManagers());

            X509TrustManager wrappedTm = new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    if (systemTm != null) systemTm.checkClientTrusted(chain, authType);
                }
                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    if (systemTm != null) {
                        try {
                            systemTm.checkServerTrusted(chain, authType);
                        } catch (CertificateException e) {
                            Log.w(TAG, "System rejected cert, allowing. " + e.getMessage());
                        }
                    }
                }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return systemTm != null ? systemTm.getAcceptedIssuers() : new X509Certificate[0];
                }
            };

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{wrappedTm}, new java.security.SecureRandom());

            return new OkHttpClient.Builder()
                    .sslSocketFactory(sslContext.getSocketFactory(), wrappedTm)
                    .hostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier())
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .build();

        } catch (Exception e) {
            Log.e(TAG, "fallback", e);
            return new OkHttpClient.Builder()
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .build();
        }
    }

    private X509TrustManager extractX509(TrustManager[] tms) {
        for (TrustManager tm : tms) {
            if (tm instanceof X509TrustManager) return (X509TrustManager) tm;
        }
        return null;
    }
}

加完 AppGlideModule 后,Rebuild Project (Build → Rebuild Project),Glide 会自动生成 GlideApp。现有代码的 Glide.with(...) 不需要改 ,因为 AppGlideModule 会自动全局生效。

然后再次运行程序,终于全部正常显示了。

总结2:Glide 网络请求与业务接口相互隔离,单独走原生网络栈,无法共享 OkHttp 的证书信任配置。必须手动接入 OkHttp 集成模块,自定义可兼容缺失根证书的客户端并注册到 Glide 全局,才能解决定制设备下 HTTPS 图片加载异常。

相关推荐
lcreek17 分钟前
Java 反序列化漏洞深度解析(一):从URLDNS到真正的DNS探测
java·反序列化漏洞
杰克尼25 分钟前
天机学堂复习总结(day03-day04)
java·开发语言·redis·elasticsearch·spring cloud
x***r1511 小时前
jdk-11.0.16.1_windows使用步骤详解(附JDK 11环境变量配置与验证教程)
java·开发语言·windows
弹简特1 小时前
【Java项目-轻聊】01-项目演示+项目介绍+准备工作+项目源码
java
luck_bor2 小时前
File类&递归作业
java·开发语言
武子康2 小时前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
REDcker4 小时前
Linux OverlayFS详解
java·linux·运维
Royzst4 小时前
xml知识点
java·服务器·前端
鱼鳞_5 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存
过期动态5 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq