原文:http://blog.csdn.net/raptor/article/details/18896375
前言
由於移動設備使用的網絡環境各種各樣,而且常常接入不安全的公共WIFI——如果你對公共WIFI環境的安全性沒有警惕性的話,就難怪你開發出不安全的程序,把你的用戶置於危險境地——這話一點都不誇張。
而要想在不安全的網絡環境下安全地使用網絡,最好的辦法就是通過VPN連接到安全網絡環境中去。但這並不總是能夠保證的。所以需要應用開發者在開發的時候儘量減少用戶的安全風險。
通過HTTPS連接網絡是一種常用的方法。但是在實際使用中存在幾個困難:
* 使用商業證書的成本
* 使用自定義證書不被系統承認
* 忽略證書驗證則可能被“中間人攻擊”
本文將針對這些問題討論技術解決方案。
因爲最近又開始試用Android Studio,所以這裏的Demo是用Android Studio 0.4.2寫的。完整的Demo代碼可以在bitbucket獲得:https://bitbucket.org/raptorz/democert
基本的HTTP連接方式
首先來看基本的HTTP連接方式實現,程序的項目框架是直接用嚮導生成後略作修改。主要就是增加一個異步網絡調用的任務,任務內容大致爲:
- HttpUriRequest request = new HttpGet(url);
- HttpClient client = DemoHttp.getClient();
- try {
- HttpResponse httpResponse = client.execute(request);
- int responseCode = httpResponse.getStatusLine().getStatusCode();
- String message = httpResponse.getStatusLine().getReasonPhrase();
- HttpEntity entity = httpResponse.getEntity();
- if (responseCode == 200 && entity != null)
- {
- return parseString(entity);
- }
- }
- finally {
- client.getConnectionManager().shutdown();
- }
- return "";
上面這個函數功能就是創建一個HttpClient去GET url的內容,如果HTTP返回值爲200(即無錯誤),則返回響應內容。
重點就在DemoHttp.getClient()裏,對於基本的HTTP連接,以下是實現部分代碼:
- public static HttpClient getClient() {
- BasicHttpParams params = new BasicHttpParams();
- HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
- HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
- HttpProtocolParams.setUseExpectContinue(params, true);
- SchemeRegistry schReg = new SchemeRegistry();
- schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
- ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
- return new DefaultHttpClient(connMgr, params);
- }
實際的實現代碼當然比上面這兩段多得多了,Java就是這麼麻煩,一點小事都要寫一大堆代碼,爲節約篇幅就不全部列出了,參見bitbucket上的完整代碼吧。
順便說一句,寫網絡通訊應用別忘記在Manifest.xml里加上相應的權限,否則會出一些很奇怪的錯誤。
HTTP連接的主要問題在於在傳輸過程中的內容都是明文,只要在同一網段內使用嗅探程序即可獲得網內其它設備與服務器之間的通訊內容,完全沒有安全性。
使用系統承認的商業證書的HTTPS連接方式
在上面的例子中,如果嘗試用https連接的話,會報錯稱不支持https: Scheme 'https' not registered。最簡單的解決辦法就是參照HTTP的方式,加入對HTTPS的支持:
- schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
關鍵代碼就這麼一句。
現在就可以像打開HTTP鏈接一樣打開有效的HTTPS連接了,比如: https://www.google.com.hk 。但可恥的 12306 的HTTPS卻不行,因爲它使用了不被系統承認的自定義證書:No peer certificate 。
這個方案使用了HTTPS連接,傳輸內容經過加密,嗅探程序已經無法獲得通訊內容。而服務器的證書經過合法簽名,被系統所承認,正常通訊也沒有問題。
但是需要花錢買證書。
使用自定義證書並忽略驗證的HTTPS連接方式
如果不想花錢,那麼就只能用OPENSSL自己做一個證書,但問題在於,這個證書得不到系統的承認,後果同 12306 。爲了解決這個問題,一個辦法是跳過系統校驗。
要跳過系統校驗,就不能再使用系統標準的SSLSocketFactory了,需要自定義一個。然後爲了在這個自定義SSLSocketFactory裏跳過校驗,還需要自定義一個TrustManager,在其中忽略所有校驗,即TrustAll。
以下就是SSLTrustAllSocketFactory和SSLTrustAllManager的實現:
- public class SSLTrustAllSocketFactory extends SSLSocketFactory {
- private static final String TAG = "SSLTrustAllSocketFactory";
- private SSLContext mCtx;
- public class SSLTrustAllManager implements X509TrustManager {
- @Override
- public void checkClientTrusted(X509Certificate[] arg0, String arg1)
- throws CertificateException {
- }
- @Override
- public void checkServerTrusted(X509Certificate[] arg0, String arg1)
- throws CertificateException {
- }
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return null;
- }
- }
- public SSLTrustAllSocketFactory(KeyStore truststore)
- throws Throwable {
- super(truststore);
- try {
- mCtx = SSLContext.getInstance("TLS");
- mCtx.init(null, new TrustManager[] { new SSLTrustAllManager() },
- null);
- setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
- } catch (Exception ex) {
- }
- }
- @Override
- public Socket createSocket(Socket socket, String host,
- int port, boolean autoClose)
- throws IOException, UnknownHostException {
- return mCtx.getSocketFactory().createSocket(socket, host, port, autoClose);
- }
- @Override
- public Socket createSocket() throws IOException {
- return mCtx.getSocketFactory().createSocket();
- }
- public static SSLSocketFactory getSocketFactory() {
- try {
- KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
- trustStore.load(null, null);
- SSLSocketFactory factory = new SSLTrustAllSocketFactory(trustStore);
- return factory;
- } catch (Throwable e) {
- Log.d(TAG, e.getMessage());
- e.printStackTrace();
- }
- return null;
- }
- }
最後在註冊scheme時使用這個Factory:
- schReg.register(new Scheme("https", SSLTrustAllSocketFactory.getSocketFactory(), 443));
這樣就可以成功打開 12306 的內容了。
不過這個方案雖然用了HTTPS,通訊的內容也經過了加密,嗅探程序也無法知道內容。但是通過更麻煩一些的“中間人攻擊”,還是可以竊取通訊內容的:
在網內配置一個DNS,把目標服務器域名解析到本地的一個地址,然後在這個地址上用一箇中間服務器作代理,它使用一個假的證書與客戶端通訊,然後再由這個代理作爲客戶端連到實際的服務器,用真的證書與服務器通訊。這樣所有的通訊內容就會經過這個代理。而因爲客戶端不校驗證書,所以它用來加密的證書實際上是代理提供的假證書,那麼代理就可以完全知道通訊內容。這個代理就是所謂的“中間人”。
但是不幸的是,網上搜到的大部分關於自定義證書的HTTPS連接實現都是用這種忽略驗證的方式實現的。
所以我們需要更安全的方式。詳見下篇。