Android Webview SSL 自簽名安全校驗解決方案


  • 服務器證書校驗主要針對 WebView 的安全問題。
  • 在 app 中需要通過 WebView 訪問 url,因為服務器采用的自簽名證書,而不是 ca 認證,使用 WebView 加載 url 的時候會顯示為空白,出現無法加載網頁的情況。
  • 使用 ca 認證的證書,在 WebView 則可以直接顯示出來,不需要特殊處理。
  • 以往針對自簽名證書的解決方案是繼承 WebViewClient 重寫 onReceivedSslError 方法,然后直接使用 handler.proceed(),該方案其實是忽略了證書,存在安全隱患。
  • 安全的方案是當出現了證書問題的時候,讀取 asserts 中保存的的根證書,然后與服務器校驗,假如通過了,繼續執行 handler.proceed(),否則執行 handler.cancel()。

WebViewClient 源碼

  • 當證書出現問題的時候,有 2 種情況。系統默認不加載該網頁
    • handler.cancel()

    • handler.proceed()

        public class WebViewClient {
      
        	public void onReceivedSslError(WebView view, SslErrorHandler handler,
        			SslError error) {
        		handler.cancel();
        	}
        	...
      
        }
      

以往不安全的解決方案

  • 當出現 ssl error 的時候,直接忽略,依舊打開網頁。

  • 繼承系統的 WebViewClient , 重寫 onReceiverSslError() ,改為 handler.process()

      public class UnSafeWebViewClient extends WebViewClient
      {
      	@Override
      	public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
      		handler.proceed();
      	}
      }
    

安全的解決方案

  • 把服務器的證書放置在 assert 文件夾,當出現 ssl error 的時候進行讀取,然后與服務器校驗,校驗通過了就加載該網頁。校驗不通過,不打開網頁,進行安全提醒。

      public class WebviewClient3 extends WebViewClient {
      	private Context context;
    
      	public WebviewClient3(Context context) {
      		this.context = context;
      	}
    
      	@Override
      	public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
      		test12306(handler, view.getUrl());
      	}
    
      	// 以 12306 的證書為例,因為 12306 的證書是自簽名的
      	private void test12306(final SslErrorHandler handler, String url) {
      		OkHttpClient.Builder builder;
      		try {
      			builder = setCertificates(new OkHttpClient.Builder(), context.getAssets().open(MainActivity.cer_protal_root));
      		} catch (IOException e) {
      			builder = new OkHttpClient.Builder();
      		}
      		Request request = new Request.Builder().url(url)
      				.build();
      		builder.build().newCall(request).enqueue(new Callback() {
      			@Override
      			public void onFailure(Call call, IOException e) {
      				Log.e("12306 error", e.getMessage());
      				handler.cancel();
      			}
    
      			@Override
      			public void onResponse(Call call, Response response) throws IOException {
      				Log.e("12306 ", response.body().string());
      				handler.proceed();
      			}
      		});
      	}
    
      	private OkHttpClient.Builder setCertificates(OkHttpClient.Builder client, InputStream... certificates) {
      		try {
      			CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
      			KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
      			keyStore.load(null);
      			int index = 0;
      			for (InputStream certificate : certificates) {
      				String certificateAlias = Integer.toString(index++);
      				keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
    
      				try {
      					if (certificate != null)
      						certificate.close();
      				} catch (IOException e) {
      				}
      			}
      			SSLContext sslContext = SSLContext.getInstance("TLS");
      			TrustManagerFactory trustManagerFactory =
      					TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
      			trustManagerFactory.init(keyStore);
      			sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
      			SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
      			X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory);
      			client.sslSocketFactory(sslSocketFactory, trustManager);
      		} catch (Exception e) {
      			e.printStackTrace();
      		}
      		return client;
      	}
      }
    
  • 以上代碼可以針對規范的自簽名證書進行校驗了。但是呢,我們的證書不規范,會出現 Hostname xxx not verified 的情況。這種情況需要對 Hostname 進行校驗。需要在 client 上添加如下代碼

          client.hostnameVerifier(new HostnameVerifier() {
              @Override
              public boolean verify(String hostname, SSLSession session) {
                  String peerHost = session.getPeerHost();//服務器返回的域名
                  try {
                      X509Certificate[] peerCertificates = (X509Certificate[]) session.getPeerCertificates();
                      for (X509Certificate c : peerCertificates) {
                          X500Principal subjectX500Principal = c.getSubjectX500Principal();
                          String name = new X500p(subjectX500Principal).getName();
                          String[] split = name.split(",");
                          for (String s : split) {
                              if (s.startsWith("CN")) {
                                  if (s.contains(hostname) && s.contains(peerHost)) {
                                      return true;
                                  }
                              }
                          }
                      }
                  } catch (SSLPeerUnverifiedException e) {
                      e.printStackTrace();
                  }
                  return false;
              }
          });
    

獲取證書兩種方法

  1. 服務器組直接給。如測試 12306 網站的時候,進入網頁,12306 會提供根證書的下載
  2. 通過網頁獲取。
    1. Chrome 瀏覽器,按 F12 選擇 Security
    2. 選擇 Certificate Error 的 View certificate
    3. 在彈出的證書,選擇詳細信息
    4. 在詳細信息頁面,點擊復制到文件,一路下一步,然后選擇保存證書的地方,就把證書成功導出來了。

證書的讀取

  • 從 assert 中讀取文件

      InputStream is = getAssets().open("root.cer");
    

生成 jks 、 cer 證書的 keytool 命令

  • 生成 jks

      keytool -genkey -alias li_server -keyalg RSA -keystore li_server.jks -validity 3600 -storepass 123456
    
  • 使用 jks 生成 cer

      keytool -export -alias li_server  -file li_server.cer  -keystore li_server.jks -storepass 123456
    

基礎知識

  • java 平台默認識別 jks 格式的證書文件, 但是 Android 平台只識別 bks 格式的證書文件

注意:

  • 使用本地服務器測試的時候,使用的 IP 地址,如:192.168.2.22,生成的服務器證書需要添加(-ext san=ip:192.168.2.22),否則會出現 Hostname xxx not verified 的問題:
    keytool -genkey -alias li_server -keyalg RSA -keystore li_server.jks -validity 3600 -storepass 123456 -ext san=ip:192.168.2.22

Tomcat 搭建 SSL 環境

  1. 百度搜索 tomcat 本地搭建

  2. 生成的證書 jks 放到 tomcat 的根目錄,如:D:\apache-tomcat

  3. 修改 server.xml 文件,在 Service 節點,添加如下代碼

     <Connector SSLEnabled="true" acceptCount="100" clientAuth="false" 
     	disableUploadTimeout="true" enableLookups="true" keystoreFile="li_server.jks" keystorePass="123456" maxSpareThreads="75" 
     	maxThreads="200" minSpareThreads="5" port="8443" 
     	protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https" 
     	secure="true" sslProtocol="TLS"/> 
    
  4. 重啟 tomcat ,然后就可以訪問 https 的地址了,端口為 8443,如:https://192.168.123.131:8443/

  5. 通過 chrome 可以看到,該網頁不安全提醒。

常見問題

  1. 證書有問題,證書來自不信任的來源
    java.security.cert.CertPathValidatorException: Trust anchor for certification path not found
  2. 配置服務器所使用的證書不具有與嘗試連接的服務器匹配的主題或主題備用名稱字段
    Hostname xxx not verified:

參考:
Android 安全之 Https 中間人攻擊漏洞:http://yaq.qq.com/blog/13
Android HostName 強驗證:http://www.cnblogs.com/fengchuxiaodai/p/5962760.html
Android WebView 手動校驗 https 證書: http://blog.csdn.net/lsyz0021/article/details/54669914
Android HTTPS : http://blog.csdn.net/lmj623565791/article/details/48129405
Android HostName XXX not verified : https://developer.android.com/training/articles/security-ssl.html#CommonHostnameProbs
SSLPeerUnverifiedException:HostName not verified: https://stackoverflow.com/questions/30745342/javax-net-ssl-sslpeerunverifiedexception-hostname-not-verified
jks 轉 bks :http://blog.csdn.net/bigboysunshine/article/details/54134382
jks 轉 bks : http://www.cnblogs.com/darkdog/p/4281555.html
Retrofit 使用 HTTPS: http://blog.csdn.net/dd864140130/article/details/52625666
解析證書亂碼問題:http://blog.csdn.net/suntongo/article/details/38864413


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM