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

posted @ 2017-06-14 22:25  熠然  阅读(19244)  评论(0编辑  收藏  举报