前言故事 最近網路新聞在流傳著一篇「Android 使用者注意!7.1.1 前舊機將不能正常瀏覽網頁」,小唯的公司也對此在做一些可能的準備動作。之所以會對這議題這麼敏感,正是因為不久前,小唯公司才因為 AddTrust External Root CA 過期 ,換了自己網站的簽署 root CA 憑證,造成在 Android 5 以下的手機,使用 WebView 時發生錯誤…
不建議的解決辦法 小唯對這件事仍是記憶猶新,當時的解決辦法是,把新憑證一起打包進 App 中,使用 Android 更替 X509TrustManager 的方式,將需要允許連線的憑證包進去。
過程中,有個錯誤做法值得一提,因為網路上會搜尋到這種做法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 final TrustManager[] tm = new TrustManager[] { new X509TrustManager() { @Override public void checkClientTrusted (java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted (java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { } @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new java.security.cert.X509Certificate[] {}; } } }; final SSLContext sslContext = SSLContext.getInstance("TLS" );sslContext.init(null , tm, new java.security.SecureRandom()); final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();OkHttpClient.Builder builder = ... builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) tm[0 ]); OkHttpClient = builder.build();
這是網路常見的做法之一,不建議的原因是還是安全性考量,默認相信所有憑證會讓惡意網頁有機可乘。
更好的做法 小唯團隊使用的做法是,建立一個全新的鏈狀 X509TrustManager
如下,它會先檢查 Android 系統憑證,再檢查公司的新憑證,除此之外的一率不放行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 public class CustomX509TrustManager implements X509TrustManager { private final List<X509TrustManager> trustManagers; public static TrustManager[] getTrustManagers(KeyStore keyStore) { return new TrustManager[] {new CustomX509TrustManager(keyStore)}; } public CustomX509TrustManager (KeyStore keystore) { this .trustManagers = new ArrayList<>(); this .trustManagers.add(getDefaultTrustManager()); this .trustManagers.add(getTrustManager(keystore)); try { this .trustManagers.add(trustManagerForCertificates(trustedCertificatesInputStream())); } catch (Exception e) { } } @Override public void checkClientTrusted (X509Certificate[] chain, String authType) throws CertificateException { for (X509TrustManager trustManager : trustManagers) { try { trustManager.checkClientTrusted(chain, authType); return ; } catch (CertificateException e) { } } throw new CertificateException("None of the TrustManagers trust this certificate chain" ); } @Override public void checkServerTrusted (X509Certificate[] chain, String authType) throws CertificateException { for (X509TrustManager trustManager : trustManagers) { try { trustManager.checkServerTrusted(chain, authType); return ; } catch (CertificateException e) { } } throw new CertificateException("None of the TrustManagers trust this certificate chain" ); } @Override public X509Certificate[] getAcceptedIssuers() { List<X509Certificate> certificates = new ArrayList<>(); for (X509TrustManager trustManager : trustManagers) { certificates.addAll(Arrays.asList(trustManager.getAcceptedIssuers())); } return certificates.toArray(new X509Certificate[0 ]); } private X509TrustManager getDefaultTrustManager () { return getTrustManager(null ); } private X509TrustManager getTrustManager (KeyStore keystore) { return getTrustManager(TrustManagerFactory.getDefaultAlgorithm(), keystore); } private X509TrustManager getTrustManager (String algorithm, KeyStore keystore) { TrustManagerFactory factory; try { factory = TrustManagerFactory.getInstance(algorithm); factory.init(keystore); List<TrustManager> trustManagerList = Arrays.asList(factory.getTrustManagers()); List<X509TrustManager> x509TrustManagerList = new ArrayList<>(); if (trustManagerList != null ) { for (TrustManager tm : trustManagerList) { if (tm instanceof X509TrustManager) { x509TrustManagerList.add((X509TrustManager) tm); } } } if (x509TrustManagerList.size() > 0 ) { return x509TrustManagerList.get(0 ); } } catch (NoSuchAlgorithmException | KeyStoreException e) { e.printStackTrace(); } return null ; } private X509TrustManager trustManagerForCertificates (InputStream in) throws GeneralSecurityException { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509" ); Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in); if (certificates.isEmpty()) { throw new IllegalArgumentException("Unexpected empty InputStream" ); } char [] password = "password" .toCharArray(); KeyStore keyStore = newEmptyKeyStore(password); int index = 0 ; for (Certificate certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificate); } KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, password); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); List<TrustManager> trustManagerList = Arrays.asList(trustManagerFactory.getTrustManagers()); List<X509TrustManager> x509TrustManagerList = new ArrayList<>(); if (trustManagerList != null ) { for (TrustManager tm : trustManagerList) { if (tm instanceof X509TrustManager) { x509TrustManagerList.add((X509TrustManager) tm); } } } if (x509TrustManagerList.size() > 0 ) { return x509TrustManagerList.get(0 ); } return null ; } private KeyStore newEmptyKeyStore (char [] password) throws GeneralSecurityException { try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); InputStream in = null ; keyStore.load(in, password); return keyStore; } catch (IOException e) { throw new AssertionError(e); } } private static InputStream trustedCertificatesInputStream () { String cert = "" + "-----BEGIN CERTIFICATE-----\n" + "-----END CERTIFICATE-----\n" ; return new Buffer().writeUtf8(cert).inputStream(); } }
以上從 https://square.github.io/okhttp/https/#customizing-trusted-certificates-kt-java 變化而來
其他做法 小唯之後又發現,okhttp 官方 github 也有類似的 sample,之後也許也能試試
okhttp 官方 github CustomTrust.java
關於 Android 7.1.1 至於 Android 7.1.1 版本之前的裝置呢?因為許多網站簽署的憑證即將到期,勢必會換成新的憑證,造成舊機 WebView 或瀏覽器可能不支援新的憑證,解決方法還是建議更新系統(如果可以的話),要不就是 App 要準備處理過期憑證的錯誤,適時提醒使用者憑證問題,以及建議的解決方式,避免一開啟網頁就遇到一片空白,甚至 App 強制關閉 😖
另一個小唯在思索的事情是,非自己產品的相關的網頁,是否該用 WebView 開啟?嚴格來說其實不應該,因為不清楚其他人會在相關網頁內,做什麼事情,尤其時當開啟 JavaScript 後,這個問題更值得所有用 App WebView 去開啟第三方網頁的團隊去思索~
相關連結: CompositeTrustManager , okhttp customizing trusted certificates , okhttp customizing trusted certificates
------------- 本文结束 App minSdkVersion 到底什麼時候才能提升啊 -------------