前言故事 最近網路新聞在流傳著一篇「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 到底什麼時候才能提升啊 -------------