webView https双向认证里面涉及的点比较多,踩了不少坑,下面介绍下基本流程和解决问题的办法。
1. 获取SSLContext,这里使用的是ca.cer和client.bks,见如下getSSLContext方法。
public class SslOkHttpClientUtils {
public static final String TAG = "SslOkHttpClientUtils";
public static final String KEY_STORE_TYPE_P12 = "PKCS12";//证书类型
private static OkHttpClient client;
private static SSLContext sslContext;
public static String sessionid;
public static String cookieval;
public static OkHttpClient getSslClient(Context context) {
try {
if(client == null) {
InputStream trustKey = context.getAssets().open("ca.cer");
InputStream clientKeyP12 = context.getAssets().open("client.p12");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
sslContext = SSLContext.getInstance("TLS");
Log.d(TAG, "getSslClient: 0");
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
Log.d(TAG, "getSslClient: 0.1");
trustStore.load(null);
// trustStore.load(trustKey, trustPassword.toCharArray());
Log.d(TAG, "getSslClient: 1");
trustStore.setCertificateEntry("0", certificateFactory.generateCertificate(trustKey));
if (trustKey != null) {
trustKey.close();
}
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
keyStore.load(clientKeyP12, "123456".toCharArray());
/* KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(clientKeyP12, clientPassword.toCharArray());*/
Log.d(TAG, "getSslClient: 2");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "123456".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
client = new OkHttpClient().newBuilder()
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
.followRedirects(false)
.followSslRedirects(false)
.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
Log.d(TAG, "verify: "+hostname);
if(hostname.equals("www.***.cn")) //指定域名
return true;
return false;
}
})
.build();
return client;
}
return client;
} catch (Exception e) {
e.printStackTrace();
android.util.Log.d(TAG, "exception222:"+e.toString());
return null;
}
}
public static SSLContext getSSLContext(Context context){
try {
if(sslContext == null) {
InputStream trustKey = context.getAssets().open("ca.cer");
InputStream clientKeyP12 = context.getAssets().open("client.bks");
sslContext = SSLContext.getInstance("TLS");
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(clientKeyP12, "123456".toCharArray());
clientKeyP12.close();
trustStore.load(null);
Log.d(TAG, "getSSLContext: 1");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
trustStore.setCertificateEntry("0", certificateFactory.generateCertificate(trustKey));
if (trustKey != null) {
trustKey.close();
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X509");
trustManagerFactory.init(trustStore);
keyManagerFactory.init(keyStore, "123456".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
Log.d(TAG, "getSSLContext: success");
}
return sslContext;
} catch (Exception e) {
e.printStackTrace();
android.util.Log.d(TAG, "exception222:"+e.toString());
return null;
}
}
}
2.webview setting设置
mWebView.setWebChromeClient(mWebChromeClient);
mWebView.getSettings().setUseWideViewPort(true);
mWebView.getSettings().setLoadWithOverviewMode(true);
mWebView.getSettings().setSupportZoom(true);// 支持缩放
mWebView.getSettings().setBuiltInZoomControls(true);// 显示放大缩小
mWebView.getSettings().setDisplayZoomControls(false);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
mWebView.getSettings().setDomStorageEnabled(true);
mWebView.getSettings().setAppCacheMaxSize(1024 * 1024 * 20);
String appCachePath = getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath();
mWebView.getSettings().setAppCachePath(appCachePath);
mWebView.getSettings().setDatabasePath(appCachePath);
mWebView.getSettings().setAppCacheEnabled(true);
mWebView.getSettings().setAllowFileAccess(true);
mWebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
mWebView.addJavascriptInterface(new WebContainer(), "WebContainer");
mWebView.setWebViewClient(new MyWebViewClient());
mWebView.getSettings().setSupportMultipleWindows(false);
mWebView.getSettings().setDatabaseEnabled(true);
mWebView.getSettings().setDefaultTextEncodingName("UTF-8");
mWebView.getSettings().setTextZoom(100);
mWebView.getSettings().setLayoutAlgorithm ( WebSettings.LayoutAlgorithm.SINGLE_COLUMN );
//取消滚动条
mWebView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
//触摸焦点起作用
mWebView.requestFocus();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mWebView.getSettings().setMixedContentMode(
WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
3.https相关的代码主要在new MyWebViewClient()这里面。
public MyWebViewClient() {
SslOkHttpClientUtils.cookieval = null;
SslOkHttpClientUtils.sessionid = null;
prepareSslPinning();
}
每次打开h5,让cookie和sessionid置位null.
@Override
public WebResourceResponse shouldInterceptRequest (final WebView view, String url) {
Log.d(TAG,"shouldInterceptRequest1:"+url);
// String url2 = url.replace("http://","https://");
if(!url.startsWith("https")) {
return null;
}
if(url.endsWith("favicon.ico"))
return null;
return processRequest(Uri.parse(url));
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebResourceResponse shouldInterceptRequest (final WebView view, WebResourceRequest interceptedRequest) {
Log.d(TAG,"shouldInterceptRequest2:");
// String url2 = interceptedRequest.getUrl().toString().replace("http://","https://");
if(!interceptedRequest.getUrl().toString().startsWith("https")) {
return null;
}
if(interceptedRequest.getUrl().toString().endsWith("favicon.ico"))
return null;
return processRequest(interceptedRequest.getUrl());
}
shouldInterceptRequest方法截获https请求,针对http请求则返回null.
private WebResourceResponse processRequest(Uri uri) {
android.util.Log.d(TAG, "processRequest url: "+uri.toString());
HttpsURLConnection urlConnection = httpsUrlRequest(uri,"GET");
try {
//获取请求的内容、contentType、encoding
android.util.Log.d(TAG, "processRequest: "+urlConnection.getResponseCode()+":"+uri.toString());
//若返回405 尝试post请求
if(urlConnection.getResponseCode() == 405){
urlConnection = httpsUrlRequest(uri,"POST");
android.util.Log.d(TAG, "processRequest: "+urlConnection.getResponseCode()+":"+uri.toString());
}
if(urlConnection.getResponseCode() == 200){
String cookie = urlConnection.getHeaderField("Set-Cookie");
if(cookie != null) {
SslOkHttpClientUtils.cookieval = cookie;
SslOkHttpClientUtils.sessionid = cookie.substring(0, cookie.indexOf(";"));
}
}
String contentType = urlConnection.getContentType();
String encoding = urlConnection.getContentEncoding();
InputStream inputStream = urlConnection.getInputStream();
if (null != contentType){
String mimeType = contentType;
if (contentType.contains(";")){
mimeType = contentType.split(";")[0].trim();
}
//返回新的response
return new WebResourceResponse(mimeType, encoding, inputStream);
}
} catch (MalformedURLException e) {
e.printStackTrace();
android.util.Log.d(TAG, "MalformedURLException: "+e.getMessage());
} catch (IOException e) {
e.printStackTrace();
android.util.Log.d(TAG, "IOException: "+e.getMessage());
}/*finally {
if(urlConnection!=null){
urlConnection.disconnect();
}
}*/
return null;
}
private HttpsURLConnection httpsUrlRequest(Uri uri ,String requstType){
HttpsURLConnection urlConnection = null;
try {
//设置连接
URL url = new URL(uri.toString());
urlConnection = (HttpsURLConnection) url.openConnection();
android.util.Log.d(TAG, "procesessionid: "+SslOkHttpClientUtils.sessionid);
if(SslOkHttpClientUtils.sessionid != null) {
urlConnection.setRequestProperty("Cookie", SslOkHttpClientUtils.sessionid);
}
//为request设置SSL Socket Factory
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
urlConnection.setConnectTimeout(3000);
urlConnection.setRequestMethod(requstType);
urlConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
return urlConnection;
} catch (MalformedURLException e) {
e.printStackTrace();
android.util.Log.d(TAG, "MalformedURLException: "+e.getMessage());
} catch (IOException e) {
e.printStackTrace();
android.util.Log.d(TAG, "IOException: "+e.getMessage());
}
return null;
}
private void prepareSslPinning() {
sslContext = SslOkHttpClientUtils.getSSLContext(mContext);
}
HttpsURLConnection建立连接,进行认证,webView涉及页面的跳转和返回,必须要在同一个session里面进行,不然涉及到登录或者token验证时就会异常,一个页面有多个https请求,涉及到的js,css都要认证,因此需要把session强制到url请求里去。后面又发现有的页面返回405,经过抓包是由于setRequestMethod方法不对,默认是get,如果页面是post请求则会报405,需要转为post.
4.资源图片加载不出来是因为对证书请求时Android默认的处理方式是取消,需要改下代码:
@Override
public void onReceivedSslError(WebView view,
SslErrorHandler handler, SslError error) {
android.util.Log.d(TAG, "onReceivedSslError: "+view.getUrl());
// TODO Auto-generated method stub
// handler.cancel();// Android默认的处理方式
handler.proceed();// 接受所有网站的证书
// handleMessage(Message msg);// 进行其他处理
}
之前http完全没有任何问题,改为https后,起初发现h5页面有的加载不出来,有的从一个页面跳到另外页面报错,或者从第二个页面返回第一个页面,第一个页面报错,经过和后台联调,发现是seesionid不一致,为何https为这样呢,可能是域名不一致,导致返回的时候需要重新请求。
经过 网上查询安卓h5session不一致问题,大概是因为android手机端在访问web服务器时,没有给http请求头部设置sessionID,而使用web浏览器作为客户端访问服务器时,在客户端每次发起请求的时候,都会将交互中的sessionID:JSESSIONID设置在Cookie头中携带过去,服务器根据这个sessionID获取对应的Session,而不是重新创建一个新Session(除了这个Session失效)。
因此需要手动传个sessionid过去。