開發手機應用程式時,很多時候都會使用到網路連線,透過網路上的Web service API取得App所需的資料。
而早前自己在開發Android App時,需求上必須要使用HTTPS作訪問,但使用HTTPS測試時就遇到了一個不明的錯誤,日誌上的錯誤訊息:javax.net.ssl.SSLPeerUnverifiedException: No peer certificate。
目前所使用的Web伺服器:Apache
當我使用瀏覽器查看HTTPS的請求時,並沒有任何警告提示,所以認為Web伺服器的SSL設置沒有問題,理應Android訪問應該也是沒問題才對吧。
此時Apache Web伺服器的SSL設置如下:
<VirtualHost *:443>
ServerAdmin webmster@localhost
DocumentRoot "/var/www/html"
ServerName default
ServerAlias *
SSLEngine on
SSLCertificateFile /etc/httpd/conf.d/ssl/server.crt
SSLCertificateKeyFile /etc/httpd/conf.d/ssl/server.key
</VirtualHost>
經過網上一番努力爬文後,有人提到Android的証書庫是帶有startssl ca証書,而Apache默認是不帶startssl ca証書,造成Android訪問Apache的驗証就會失敗。另外,網上亦提到另一jetty Web伺服器默認是帶有startssl ca証書。為了証實是否因startssl ca証書引起,於是我便嘗試架設一個jetty Web伺服器做測試。
架設完成後,我便開始從程式碼下手,在需要使用HTTPS的地方加入如下程式碼:
private static void trustAllHosts() {
final String TAG = "trustAllHosts";
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
Log.i(TAG, "checkClientTrusted");
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
Log.i(TAG, "checkServerTrusted");
}
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
} };
// Install the all-trusting trust manager
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
}
從程式裹可見,我為HTTPS的連線設置了自訂的証書管理物件,現在所有HTTPS的訪問都必須經過此証書管理物件。
在checkServerTrusted函式加入斷點進行除錯,查看java.security.cert.X509Certificate[] chain物件的值(chain物件為証書訊息),發現訪問兩個Web伺服器的chain值是不同的,而Apache回傳的証書訊息裹缺少了startssl ca証書,所以迷底終於解開,只要將Apache設置好startssl ca証書,Android端便可正常訪問。
加入startssl ca後,再次使用Android訪問便沒有報錯,詳細的Apache設置可到startssl官網查看教學,官網內亦有提供其它Web伺服器的設置教學。編輯後Apache Web伺服器的SSL設置如下:
<VirtualHost *:443>
ServerAdmin webmaster@localhost
DocumentRoot "/var/www/html"
ServerName default
ServerAlias *
SSLEngine on
SSLCertificateFile /etc/httpd/conf.d/ssl/server.crt
SSLCertificateKeyFile /etc/httpd/conf.d/ssl/server.key
SSLCertificateChainFile /usr/local/apache/conf/sub.class1.server.ca.pem
</VirtualHost>
但很多時候在開發階段,開發者只能訪問一個使用self-signed証書的Web service,所以Android一定會出現相同的錯誤訊息javax.net.ssl.SSLPeerUnverifiedException: No peer certificate,測試時實在不方便。
為了能方便開發者進行測試,在網路上有找到另一個解決方法是實作一個SSLSocketFactory來解決,此解決方式是相信所有証書,不對証書進行驗証,所以亦能正常訪問HTTPS。
MySSLSocketFactory.java 自行實作的SSLSocketFactory
package com.vvtitan.testhttps;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
public class MySSLSocketFactory extends SSLSocketFactory {
SSLContext mSSLContext = SSLContext.getInstance("TLS");
@Override
public Socket createSocket() throws IOException {
return mSSLContext.getSocketFactory().createSocket();
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return mSSLContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager mTrustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
mSSLContext.init(null, new TrustManager[] { mTrustManager }, null);
}
public static HttpClient createMyHttpClient() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load( null, null);
SSLSocketFactory mSSLSocketFactory = new MySSLSocketFactory(trustStore);
mSSLSocketFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", mSSLSocketFactory, 443));
ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);
return new DefaultHttpClient(ccm, params);
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
}
return new DefaultHttpClient();
}
}
訪問HTTPS的主程式
HttpGet request = new HttpGet("https://domain.com");
HttpClient client = MySSLSocketFactory. createMyHttpClient();
HttpParams params = client.getParams();
HttpResponse httpResponse;
try {
httpResponse = client.execute(request);
} catch (ClientProtocolException e) {
client.getConnectionManager().shutdown();
e.printStackTrace();
} catch (IOException e) {
client.getConnectionManager().shutdown();
e.printStackTrace();
}
詳細如何實作,可參考範例(TestHttps.zip)。