最近做一个安全级别比较高的项目,对方要求使用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
package com.cloudhome.webview_https;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
/**
* Created by mennomorsink on 06/05/15.
*/
public class SslPinningWebViewClient extends WebViewClient {
private LoadedListener listener;
private SSLContext sslContext;
public SslPinningWebViewClient(LoadedListener listener)throws IOException {
this.listener = listener;
prepareSslPinning(MyApplication.mContext.getResources().getAssets().open("server.cer"));
}
@Override
public WebResourceResponse shouldInterceptRequest (final WebView view, String url) {
if(MainActivity.pinningSwitch.isChecked()) {
return processRequest(Uri.parse(url));
} else {
return null;
}
}
@Override
@TargetApi(21)
public WebResourceResponse shouldInterceptRequest (final WebView view, WebResourceRequest interceptedRequest) {
if(MainActivity.pinningSwitch.isChecked()) {
return processRequest(interceptedRequest.getUrl());
} else {
return null;
}
}
private WebResourceResponse processRequest(Uri uri) {
Log.d("SSL_PINNING_WEBVIEWS", "GET: " + uri.toString());
try {
// Setup connection
URL url = new URL(uri.toString());
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// Set SSL Socket Factory for this request
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
// Get content, contentType and encoding
InputStream is = urlConnection.getInputStream();
String contentType = urlConnection.getContentType();
String encoding = urlConnection.getContentEncoding();
// If got a contentType header
if(contentType != null) {
String mimeType = contentType;
// Parse mime type from contenttype string
if (contentType.contains(";")) {
mimeType = contentType.split(";")[0].trim();
}
Log.d("SSL_PINNING_WEBVIEWS", "Mime: " + mimeType);
listener.Loaded(uri.toString());
// Return the response
return new WebResourceResponse(mimeType, encoding, is);
}
} catch (SSLHandshakeException e) {
if(isCause(CertPathValidatorException.class, e)) {
listener.PinningPreventedLoading(uri.getHost());
}
Log.d("SSL_PINNING_WEBVIEWS", e.getLocalizedMessage());
} catch (Exception e) {
Log.d("SSL_PINNING_WEBVIEWS", e.getLocalizedMessage());
}
// Return empty response for this request
return new WebResourceResponse(null, null, null);
}
private void prepareSslPinning(InputStream... certificates)throws IOException {
try {
InputStream inputStream = MyApplication.mContext.getResources().getAssets().open("client.p12");
TrustManager[] trustManagers = prepareTrustManager(certificates);
KeyManager[] keyManagers = prepareKeyManager(inputStream, "your client.p12 password");
sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, new TrustManager[]{new MyTrustManager(chooseTrustManager(trustManagers))}, new SecureRandom());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
}
private static TrustManager[] prepareTrustManager(InputStream... certificates)
{
if (certificates == null || certificates.length <= 0) return null;
try
{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
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)
{
}
}
TrustManagerFactory trustManagerFactory = null;
trustManagerFactory = TrustManagerFactory.
getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
return trustManagers;
} catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
} catch (CertificateException e)
{
e.printStackTrace();
} catch (KeyStoreException e)
{
e.printStackTrace();
} catch (Exception e)
{
e.printStackTrace();
}
return null;
}
private static KeyManager[] prepareKeyManager(InputStream bksFile, String password)
{
try
{
if (bksFile == null || password == null) return null;
KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
clientKeyStore.load(bksFile, password.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, password.toCharArray());
return keyManagerFactory.getKeyManagers();
} catch (KeyStoreException e)
{
e.printStackTrace();
} catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
} catch (UnrecoverableKeyException e)
{
e.printStackTrace();
} catch (CertificateException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
} catch (Exception e)
{
e.printStackTrace();
}
return null;
}
private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers)
{
for (TrustManager trustManager : trustManagers)
{
if (trustManager instanceof X509TrustManager)
{
return (X509TrustManager) trustManager;
}
}
return null;
}
private static class MyTrustManager implements X509TrustManager
{
private X509TrustManager defaultTrustManager;
private X509TrustManager localTrustManager;
public MyTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException
{
TrustManagerFactory var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
var4.init((KeyStore) null);
defaultTrustManager = chooseTrustManager(var4.getTrustManagers());
this.localTrustManager = localTrustManager;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
try
{
defaultTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException ce)
{
localTrustManager.checkServerTrusted(chain, authType);
}
}
@Override
public X509Certificate[] getAcceptedIssuers()
{
return new X509Certificate[0];
}
}
public static boolean isCause(
Class<? extends Throwable> expected,
Throwable exc
) {
return expected.isInstance(exc) || (
exc != null && isCause(expected, exc.getCause())
);
}
}
step3:重写的WebViewClient :SslPinningWebViewClient加入到webview中
package com.cloudhome.webview_https;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.Switch;
import android.widget.TextView;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private WebView webView;
public static Switch pinningSwitch;
private Button btnA;
private Button btnB;
public static TextView textView;
private String url1 = "your https url";
private String url2 = "your https url";
public MainActivity() {
}
public static Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = (WebView)findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
pinningSwitch = (Switch)findViewById(R.id.pinningSwitch);
btnA = (Button)findViewById(R.id.btn1);
btnB = (Button)findViewById(R.id.btn2);
textView = (TextView)findViewById(R.id.textView);
mContext=this;
SslPinningWebViewClient webViewClient = null;
try {
webViewClient = new SslPinningWebViewClient(new LoadedListener() {
@Override
public void Loaded(final String url) {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText("Loaded " + url);
}
});
}
@Override
public void PinningPreventedLoading(final String host) {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText("SSL Pinning prevented loading from " + host);
}
});
}
});
} catch (IOException e) {
e.printStackTrace();
}
webView.setWebViewClient(webViewClient);
btnA.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
webView.clearView();
textView.setText("");
webView.loadUrl(url1);
}
});
btnB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
webView.clearView();
textView.setText("");
webView.loadUrl(url2);
}
});
}
}
step4:下载回调监听借口LoadedListener借口(根据实际情况使用)
package com.cloudhome.webview_https;
/**
* Created by mennomorsink on 25/07/15.
*/
public interface LoadedListener {
void Loaded(String url);
void PinningPreventedLoading(String host);
}
运行效果:
开始前
运行中加载图片(链接中有其它https请求)
最后加载完成