編輯:關於Android編程
之前寫過一篇使用HttpClient來實現Android平台HTTPS通信的文章,收到很多讀者的私信。悲催的是,私信內容我今天才看見。由於之前是使用HttpClient來實現Android平台的HTTPS通信,但是HttpClient在Android2.2之後就不推薦使用了,所以這裡重寫這篇博客,將所有HTTPS通信代碼改用HttpUrlConnection實現。同時,講解完成後,還會寫一篇文章來講述如何使用Volley來實現HTTPS通信.
HTTPS(Hyper Text Transfer Protocol Secure),是一種基於SSL/TLS的HTTP,所有的HTTP數據都是在SSL/TLS協議封裝之上進行傳輸的。HTTPS協議是在HTTP協議的基礎上,添加了SSL/TLS握手以及數據加密傳輸,也屬於應用層協議。所以,研究HTTPS協議原理,最終就是研究SSL/TLS協議。
SSL/TLS協議作用
不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文傳播,帶來了三大風險:
竊聽風險:第三方可以獲知通信內容。篡改風險:第三方可以修改通知內容。冒充風險:第三方可以冒充他人身份參與通信。SSL/TLS協議是為了解決這三大風險而設計的,希望達到:
所有信息都是加密傳輸,第三方無法竊聽。具有校驗機制,一旦被篡改,通信雙方都會立刻發現。配備身份證書,防止身份被冒充。基本的運行過程:
SSL/TLS協議的基本思路是采用公鑰加密法,也就是說,客戶端先向服務器端索要公鑰,然後用公鑰加密信息,服務器收到密文後,用自己的私鑰解密。但是這裡需要了解兩個問題的解決方案。
如何保證公鑰不被篡改?
解決方法:將公鑰放在數字證書中。只要證書是可信的,公鑰就是可信的。公鑰加密計算量太大,如何減少耗用的時間?
解決方法:每一次對話(session),客戶端和服務器端都生成一個“對話密鑰”(session key),用它來加密信息。由於“對話密鑰”是對稱加密,所以運算速度非常快,而服務器公鑰只用於加密“對話密鑰”本身,這樣就減少了加密運算的消耗時間。
因此,SSL/TLS協議的基本過程是這樣的:
客戶端向服務器端索要並驗證公鑰。雙方協商生成“對話密鑰”。雙方采用“對話密鑰”進行加密通信。上面過程的前兩布,又稱為“握手階段”。
“握手階段”涉及四次通信,需要注意的是,“握手階段”的所有通信都是明文的。
首先,客戶端(通常是浏覽器)先向服務器發出加密通信的請求,這被叫做ClientHello請求。在這一步中,客戶端主要向服務器提供以下信息:
支持的協議版本,比如TLS 1.0版一個客戶端生成的隨機數,稍後用於生成“對話密鑰”。支持的加密方法,比如RSA公鑰加密。支持的壓縮方法。這裡需要注意的是,客戶端發送的信息之中不包括服務器的域名。也就是說,理論上服務器只能包含一個網站,否則會分不清應用向客戶端提供哪一個網站的數字證書。這就是為什麼通常一台服務器只能有一張數字證書的原因。
服務器收到客戶端請求後,向客戶端發出回應,這叫做ServerHello。服務器的回應包含以下內容:
確認使用的加密通信協議版本,比如TLS 1.0版本。如果浏覽器與服務器支持的版本不一致,服務器關閉加密通信。一個服務器生成的隨機數,稍後用於生成“對話密鑰”。確認使用的加密方法,比如RSA公鑰加密。服務器證書。除了上面這些信息,如果服務器需要確認客戶端的身份,就會再包含一項請求,要求客戶端提供“客戶端證書”。比如,金融機構往往只允許認證客戶連入自己的網絡,就會向正式客戶提供USB密鑰,裡面就包含了一張客戶端證書。
客戶端收到服務器回應以後,首先驗證服務器證書。如果證書不是可信機構頒發,或者證書中的域名與實際域名不一致,或者證書已經過期,就會向訪問者顯示一個警告,由其選擇是否還要繼續通信。
如果證書沒有問題,客戶端就會從證書中取出服務器的公鑰。然後,向服務器發送下面三項消息。
上面第一項隨機數,是整個握手階段出現的第三個隨機數,又稱“pre-master key”。有了它以後,客戶端和服務器就同時有了三個隨機數,接著雙方就用事先商定的加密方法,各自生成本次會話所用的同一把“會話密鑰”。
服務器收到客戶端的第三個隨機數pre-master key之後,計算生成本次會話所用的“會話密鑰”。然後,向客戶端最後發送下面信息。
編碼改變通知,表示隨後的信息都將用雙方商定的加密方法和密鑰發送。服務器握手結束通知,表示服務器的握手階段已經結束。這一項同時也是前面發生的所有內容的hash值,用來供客戶端校驗。至此,整個握手階段全部結束。接下來,客戶端與服務器進入加密通信,就完全是使用普通的HTTP協議,只不過用“會話密鑰”加密內容。
之前使用了HttpClient來實現HTTPS通信,而且代碼中有大量無關代碼,自己回顧看起來都特別混亂.所以,這裡只列出HttpUrlConnection實現HTTPS通信的關鍵代碼。
我們以百度的https網址(https://m.baidu.com/)為例,示例源碼如下:
public void startHttpsConnection() {
HttpsURLConnection httpsURLConnection = null;
BufferedReader reader = null;
try {
URL url = new URL("https://m.baidu.com/");
httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setConnectTimeout(5000);
httpsURLConnection.setDoInput(true);
httpsURLConnection.setUseCaches(false);
httpsURLConnection.connect();
reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
StringBuilder sBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sBuilder.append(line);
}
Log.e("TAG", "Wiki content=" + sBuilder.toString());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (httpsURLConnection != null) {
httpsURLConnection.disconnect();
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
由於百度是有CA授權的數字證書,所以這裡我們就是簡單的使用HttpsUrlConnection對其進行訪問,就實現了HTTPS通信。
由於CA認證是需要收費的,所以有些網站為了節約成本,采用自簽名的數字證書,偉大的12306目前依然是這麼干的。如果我們用上述代碼訪問自簽名的網站會有什麼問題呢?
截取一段crash信息如下:
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:409)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.Connection.upgradeToTls(Connection.java:153)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.Connection.connect(Connection.java:114)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:298)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:259)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.internal.http.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:89)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.android.okhttp.internal.http.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:161)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.genius.wzy.MainActivity.startHttpsConnection(MainActivity.java:58)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at com.genius.wzy.MainActivity$1.run(MainActivity.java:34)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: at java.lang.Thread.run(Thread.java:841)
可以看到,訪問自簽名證書的網站,Android直接會throw SSLHandshakeException,原因就是12306的數字證書不被Android系統的信任。想解決這個問題,有如下幾種方法。
這是網上資源最多也是最不靠譜的解決方案。具體實現方法如下。
Step1. 實現X509TrustManager接口,在接口實現中跳過客戶端和服務器端認證。
public class TrustAllCertsManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
// Do nothing -> accept any certificates
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
// Do nothing -> accept any certificates
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
Step2. 實現HostnameVerifier接口,不進行url和服務器主機名的驗證。
public class VerifyEverythingHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
Step3. 基於上面實現的TrustAllCertsManager修改HttpsURLConnection類的默認SSL socket factory。
TrustManager[] trustManager = new TrustManager[] {new TrustEverythingTrustManager()};
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManager, new java.security.SecureRandom());
} catch (NoSuchAlgorithmException e) {
// do nothing
}catch (KeyManagementException e) {
// do nothing
}
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
Setp4. 實例化HttpsUrlConnection,並設置HostnameVerifier為上面實現的VerifyEverythingHostnameVerifier。
httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setHostnameVerifier(new VerifyEverythingHostnameVerifier());
上述四個步驟,就可以讓你無障礙的訪問自簽名的HTTPS網站了,例如12306。但是,這種方式雖然簡單,但是會導致嚴重的安全問題,例如臭名昭著的中間人攻擊。
中間人攻擊
雖然上述方案使用了HTTPS,客戶端和服務器端的通信內容得到了加密,嗅探程序無法得到傳輸的內容,但是無法抵擋“中間人攻擊”。例如,在內網配置一個DNS,把目標服務器域名解析到本地的一個地址,然後在這個地址上使用一個中間服務器作為代理,它使用一個假的證書與客戶端通訊,然後再由這個代理服務器作為客戶端連接到實際的服務器,用真的證書與服務器通訊。這樣所有的通訊內容都會經過這個代理,而客戶端不會感知,這是由於客戶端不校驗服務器公鑰證書導致的。
所以,千萬不要在生產代碼中使用上述方法解決HTTPS無法連接的問題。
為了防止上面方案可能導致的“中間人攻擊”,我們可以事先下載服務器端公鑰證書,然後將公鑰證書編譯到Android應用中,由應用自己來驗證證書。也就是我們來教會HttpsUrlConnection來認識特定的自簽名網站。還是以12306網站為例。
Step2. 將下載的證書放到應用的assets目錄下.
app->src->main->assets->srca.cer
(ps:使用Android Studio的同學需要特別注意默認asserts目錄的位置)。
Setp3. 構造特定的TrustManager[]數組.
private TrustManager[] createTrustManager() {
BufferedInputStream cerInputStream = null;
try {
// 獲取客戶端存放的服務器公鑰證書
cerInputStream = new BufferedInputStream(getAssets().open("srca.cer"));
// 根據公鑰證書生成Certificate對象
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate ca = cf.generateCertificate(cerInputStream);
Log.e("TAG", "ca=" + ((X509Certificate) ca).getSubjectDN());
// 生成包含當前CA證書的keystore
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// 使用包含指定CA證書的keystore生成TrustManager[]數組
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
return tmf.getTrustManagers();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} finally {
if (cerInputStream != null) {
try {
cerInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
Step4. 初始化SSLContext.
SSLContext sc = SSLContext.getInstance("SSL");
TrustManager[] trustManagers = createTrustManager();
if (trustManagers == null) {
Log.e("TAG", "tmf create failed!");
return;
}
sc.init(null, trustManagers, new SecureRandom());
URL url = new URL("https://kyfw.12306.cn/otn/login/init");
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
基本了解了java語法,下一步,我們一起開啟hello world的神秘之旅。 (一)android開發環境搭建 之前搭建android開發環境是件非常費力的事情,
屏幕亮度自動調節:主要是從Sensor分析之中分離出來分析LIGHT 光線感應器,因此就分析一下自動調節屏幕亮度(手機隨著光線的強度自我調節,也就是在亮的光線下屏幕自動調
了解Android控件的觸摸事件傳遞與處理對我們日常開發中自定義控件和觸摸事件沖突解決有重大意義。Android控件的觸摸事件傳遞和處理主要有以下幾個方法,下面一一介紹。
紅米pro於昨日剛剛與大家見面,對於這款售價為1499的高端紅米手機,很多人將它與樂視2pro進行對比,那麼到底紅米pro和樂視2pro哪個好呢?相信很多用