Android 让WebView完美支持https双向认证(SSL)

转自:http://blog.csdn.net/kpioneer123/article/details/51491739

这是@ happyzhang0502 关于webview https的建议:

最近做一个安全级别比较高的项目,对方要求使用HTTPS双向认证来访问web网页。双向认证在android5.0以上很好解决,但是在Android5.0以下,webviewclient中没有客户端向服务器发送证书的回调接口(回调是个隐藏函数)。

网上搜索到大概有这么几种解决方法:

1.      利用反射调用隐藏函数(不太现实,这个方法为回调方法)

2.      自己编译完整的class.jar(试过了,没成功,成本很大)

3.      重写webview(不可能,工作量巨大)

经过上面的几种想法后来在网上高人的指点下有了第四种方法。

解决方法 :拦截Webview的Request的请求,然后自己实现httpconnection捞取数据,然后返回新的 WebResourceResponse 给Webview。重写webviewclient中的shouldInterceptRequest方法即可。


下面我来给大家具体实现方法:

step1: 生成 两个证书

server.cer  服务器端证书
client.p12   客户端证书 (需要记住密码加入之后的java文件中)

将这两个证书放在 Android assets 文件下。


step2: 重写WebViewClient引入两个证书

SslPinningWebViewClient.java


  1. package com.cloudhome.webview_https;
  2. import android.annotation.TargetApi;
  3. import android.content.Context;
  4. import android.net.Uri;
  5. import android.util.Base64;
  6. import android.util.Log;
  7. import android.webkit.WebResourceRequest;
  8. import android.webkit.WebResourceResponse;
  9. import android.webkit.WebView;
  10. import android.webkit.WebViewClient;
  11. import java.io.ByteArrayInputStream;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.net.URL;
  15. import java.security.KeyManagementException;
  16. import java.security.KeyStore;
  17. import java.security.KeyStoreException;
  18. import java.security.NoSuchAlgorithmException;
  19. import java.security.SecureRandom;
  20. import java.security.UnrecoverableKeyException;
  21. import java.security.cert.CertPathValidatorException;
  22. import java.security.cert.CertificateException;
  23. import java.security.cert.CertificateFactory;
  24. import java.security.cert.X509Certificate;
  25. import javax.net.ssl.HttpsURLConnection;
  26. import javax.net.ssl.KeyManager;
  27. import javax.net.ssl.KeyManagerFactory;
  28. import javax.net.ssl.SSLContext;
  29. import javax.net.ssl.SSLHandshakeException;
  30. import javax.net.ssl.TrustManager;
  31. import javax.net.ssl.TrustManagerFactory;
  32. import javax.net.ssl.X509TrustManager;
  33. /**
  34. * Created by mennomorsink on 06/05/15.
  35. */
  36. public class SslPinningWebViewClient extends WebViewClient {
  37. private LoadedListener listener;
  38. private SSLContext sslContext;
  39. public SslPinningWebViewClient(LoadedListener listener) throws IOException  {
  40. this .listener = listener;
  41. prepareSslPinning(MyApplication.mContext.getResources().getAssets().open( "server.cer" ));
  42. }
  43. @Override
  44. public WebResourceResponse shouldInterceptRequest ( final WebView view, String url) {
  45. if (MainActivity.pinningSwitch.isChecked()) {
  46. return processRequest(Uri.parse(url));
  47. } else {
  48. return null ;
  49. }
  50. }
  51. @Override
  52. @TargetApi ( 21 )
  53. public WebResourceResponse shouldInterceptRequest ( final WebView view, WebResourceRequest interceptedRequest) {
  54. if (MainActivity.pinningSwitch.isChecked()) {
  55. return processRequest(interceptedRequest.getUrl());
  56. } else {
  57. return null ;
  58. }
  59. }
  60. private WebResourceResponse processRequest(Uri uri) {
  61. Log.d( "SSL_PINNING_WEBVIEWS" , "GET: " + uri.toString());
  62. try {
  63. // Setup connection
  64. URL url = new URL(uri.toString());
  65. HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
  66. // Set SSL Socket Factory for this request
  67. urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
  68. // Get content, contentType and encoding
  69. InputStream is = urlConnection.getInputStream();
  70. String contentType = urlConnection.getContentType();
  71. String encoding = urlConnection.getContentEncoding();
  72. // If got a contentType header
  73. if (contentType != null ) {
  74. String mimeType = contentType;
  75. // Parse mime type from contenttype string
  76. if (contentType.contains( ";" )) {
  77. mimeType = contentType.split( ";" )[ 0 ].trim();
  78. }
  79. Log.d( "SSL_PINNING_WEBVIEWS" , "Mime: " + mimeType);
  80. listener.Loaded(uri.toString());
  81. // Return the response
  82. return new WebResourceResponse(mimeType, encoding, is);
  83. }
  84. } catch (SSLHandshakeException e) {
  85. if (isCause(CertPathValidatorException. class , e)) {
  86. listener.PinningPreventedLoading(uri.getHost());
  87. }
  88. Log.d( "SSL_PINNING_WEBVIEWS" , e.getLocalizedMessage());
  89. } catch (Exception e) {
  90. Log.d( "SSL_PINNING_WEBVIEWS" , e.getLocalizedMessage());
  91. }
  92. // Return empty response for this request
  93. return new WebResourceResponse( null , null , null );
  94. }
  95. private void prepareSslPinning(InputStream... certificates) throws IOException {
  96. try {
  97. InputStream inputStream = MyApplication.mContext.getResources().getAssets().open( "client.p12" );
  98. TrustManager[] trustManagers = prepareTrustManager(certificates);
  99. KeyManager[] keyManagers = prepareKeyManager(inputStream, "your client.p12 password" );
  100. sslContext = SSLContext.getInstance( "TLS" );
  101. sslContext.init(keyManagers, new TrustManager[]{ new MyTrustManager(chooseTrustManager(trustManagers))}, new SecureRandom());
  102. } catch (NoSuchAlgorithmException e) {
  103. e.printStackTrace();
  104. } catch (KeyStoreException e) {
  105. e.printStackTrace();
  106. } catch (KeyManagementException e) {
  107. e.printStackTrace();
  108. }
  109. }
  110. private static TrustManager[] prepareTrustManager(InputStream... certificates)
  111. {
  112. if (certificates == null || certificates.length <= 0 ) return null ;
  113. try
  114. {
  115. CertificateFactory certificateFactory = CertificateFactory.getInstance( "X.509" );
  116. KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
  117. keyStore.load( null );
  118. int index = 0 ;
  119. for (InputStream certificate : certificates)
  120. {
  121. String certificateAlias = Integer.toString(index++);
  122. keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
  123. try
  124. {
  125. if (certificate != null )
  126. certificate.close();
  127. } catch (IOException e)
  128. {
  129. }
  130. }
  131. TrustManagerFactory trustManagerFactory = null ;
  132. trustManagerFactory = TrustManagerFactory.
  133. getInstance(TrustManagerFactory.getDefaultAlgorithm());
  134. trustManagerFactory.init(keyStore);
  135. TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
  136. return trustManagers;
  137. } catch (NoSuchAlgorithmException e)
  138. {
  139. e.printStackTrace();
  140. } catch (CertificateException e)
  141. {
  142. e.printStackTrace();
  143. } catch (KeyStoreException e)
  144. {
  145. e.printStackTrace();
  146. } catch (Exception e)
  147. {
  148. e.printStackTrace();
  149. }
  150. return null ;
  151. }
  152. private static KeyManager[] prepareKeyManager(InputStream bksFile, String password)
  153. {
  154. try
  155. {
  156. if (bksFile == null || password == null ) return null ;
  157. KeyStore clientKeyStore = KeyStore.getInstance( "PKCS12" );
  158. clientKeyStore.load(bksFile, password.toCharArray());
  159. KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
  160. keyManagerFactory.init(clientKeyStore, password.toCharArray());
  161. return keyManagerFactory.getKeyManagers();
  162. } catch (KeyStoreException e)
  163. {
  164. e.printStackTrace();
  165. } catch (NoSuchAlgorithmException e)
  166. {
  167. e.printStackTrace();
  168. } catch (UnrecoverableKeyException e)
  169. {
  170. e.printStackTrace();
  171. } catch (CertificateException e)
  172. {
  173. e.printStackTrace();
  174. } catch (IOException e)
  175. {
  176. e.printStackTrace();
  177. } catch (Exception e)
  178. {
  179. e.printStackTrace();
  180. }
  181. return null ;
  182. }
  183. private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers)
  184. {
  185. for (TrustManager trustManager : trustManagers)
  186. {
  187. if (trustManager instanceof X509TrustManager)
  188. {
  189. return (X509TrustManager) trustManager;
  190. }
  191. }
  192. return null ;
  193. }
  194. private static class MyTrustManager implements X509TrustManager
  195. {
  196. private X509TrustManager defaultTrustManager;
  197. private X509TrustManager localTrustManager;
  198. public MyTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException
  199. {
  200. TrustManagerFactory var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
  201. var4.init((KeyStore) null );
  202. defaultTrustManager = chooseTrustManager(var4.getTrustManagers());
  203. this .localTrustManager = localTrustManager;
  204. }
  205. @Override
  206. public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
  207. {
  208. }
  209. @Override
  210. public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
  211. {
  212. try
  213. {
  214. defaultTrustManager.checkServerTrusted(chain, authType);
  215. } catch (CertificateException ce)
  216. {
  217. localTrustManager.checkServerTrusted(chain, authType);
  218. }
  219. }
  220. @Override
  221. public X509Certificate[] getAcceptedIssuers()
  222. {
  223. return new X509Certificate[ 0 ];
  224. }
  225. }
  226. public static boolean isCause(
  227. Class<? extends Throwable> expected,
  228. Throwable exc
  229. ) {
  230. return expected.isInstance(exc) || (
  231. exc != null && isCause(expected, exc.getCause())
  232. );
  233. }
  234. }


step3: 重写的WebViewClient :SslPinningWebViewClient加入到webview中


  1. package com.cloudhome.webview_https;
  2. import android.content.Context;
  3. import android.os.Bundle;
  4. import android.support.v7.app.AppCompatActivity;
  5. import android.view.View;
  6. import android.webkit.WebView;
  7. import android.widget.Button;
  8. import android.widget.Switch;
  9. import android.widget.TextView;
  10. import java.io.IOException;
  11. public class MainActivity extends AppCompatActivity {
  12. private WebView webView;
  13. public static Switch pinningSwitch;
  14. private Button btnA;
  15. private Button btnB;
  16. public static TextView textView;
  17. private String url1 = "your https url" ;
  18. private String url2 = "your https url" ;
  19. public MainActivity() {
  20. }
  21. public static Context mContext;
  22. @Override
  23. protected void onCreate(Bundle savedInstanceState) {
  24. super .onCreate(savedInstanceState);
  25. setContentView(R.layout.activity_main);
  26. webView = (WebView)findViewById(R.id.webView);
  27. webView.getSettings().setJavaScriptEnabled( true );
  28. pinningSwitch = (Switch)findViewById(R.id.pinningSwitch);
  29. btnA = (Button)findViewById(R.id.btn1);
  30. btnB = (Button)findViewById(R.id.btn2);
  31. textView = (TextView)findViewById(R.id.textView);
  32. mContext= this ;
  33. SslPinningWebViewClient webViewClient = null ;
  34. try {
  35. webViewClient = new SslPinningWebViewClient( new LoadedListener() {
  36. @Override
  37. public void Loaded( final String url) {
  38. runOnUiThread( new Runnable() {
  39. @Override
  40. public void run() {
  41. textView.setText( "Loaded " + url);
  42. }
  43. });
  44. }
  45. @Override
  46. public void PinningPreventedLoading( final String host) {
  47. runOnUiThread( new Runnable() {
  48. @Override
  49. public void run() {
  50. textView.setText( "SSL Pinning prevented loading from " + host);
  51. }
  52. });
  53. }
  54. });
  55. } catch (IOException e) {
  56. e.printStackTrace();
  57. }
  58. webView.setWebViewClient(webViewClient);
  59. btnA.setOnClickListener( new View.OnClickListener() {
  60. @Override
  61. public void onClick(View v) {
  62. webView.clearView();
  63. textView.setText( "" );
  64. webView.loadUrl(url1);
  65. }
  66. });
  67. btnB.setOnClickListener( new View.OnClickListener() {
  68. @Override
  69. public void onClick(View v) {
  70. webView.clearView();
  71. textView.setText( "" );
  72. webView.loadUrl(url2);
  73. }
  74. });
  75. }
  76. }


step4: 下载回调监听借口LoadedListener借口(根据实际情况使用)


  1. package com.cloudhome.webview_https;
  2. /**
  3. * Created by mennomorsink on 25/07/15.
  4. */
  5. public interface LoadedListener {
  6. void Loaded(String url);
  7. void PinningPreventedLoading(String host);
  8. }


运行效果:

开始前



运行中加载图片(链接中有其它https请求)


最后加载完成




参考资料:

http://blog.csdn.net/lmj623565791/article/details/48129405

http://blog.csdn.net/zhangyong125/article/details/50562865