Android網絡--我是怎么做的: Volley+OkHttp+Https
使用 OkHttp 作為傳輸層的實現.
Volley 默認根據 Android 系統版本使用不同的 Http 傳輸協議實現.
在 Android 3.0 以上 Volley 使用 ApacheHttpStack 作為傳輸協議, 在2.3 及以下使用 HttpURLConnection 作為傳輸層協議
OkHttp 相較于其它的實現有以下的優點.
- 支持SPDY,允許連接同一主機的所有請求分享一個socket。
- 如果SPDY不可用,會使用連接池減少請求延遲。
- 使用GZIP壓縮下載內容,且壓縮操作對用戶是透明的。
- 利用響應緩存來避免重復的網絡請求。
- 當網絡出現問題的時候,OKHttp會依然有效,它將從常見的連接問題當中恢復。
- 如果你的服務端有多個IP地址,當第一個地址連接失敗時,OKHttp會嘗試連接其他的地址,這對IPV4和IPV6以及寄宿在多個數據中心的服務而言,是非常有必要的。
因此使用 OkHttp 作為替代是好的選擇.
-
首
先用 OkHttp 實現一個新的 HurlStack 用于構建 Volley 的 requestQueue.- public class OkHttpStack extends HurlStack {
- private OkHttpClient okHttpClient;
- /**
- * Create a OkHttpStack with default OkHttpClient.
- */
- public OkHttpStack() {
- this(new OkHttpClient());
- }
- /**
- * Create a OkHttpStack with a custom OkHttpClient
- * @param okHttpClient Custom OkHttpClient, NonNull
- */
- public OkHttpStack(OkHttpClient okHttpClient) {
- this.okHttpClient = okHttpClient;
- }
- @Override
- protected HttpURLConnection createConnection(URL url) throws IOException {
- OkUrlFactory okUrlFactory = new OkUrlFactory(okHttpClient);
- return okUrlFactory.open(url);
- }
- }
-
然后使用 OkHttpStack 創建新的 Volley requestQueue.
- requestQueue = Volley.newRequestQueue(getContext(), new OkHttpStack());
- requestQueue.start();
這樣就行了.
使用 Https
作為一個有節操的開發者應該使用 Https 來保護用戶的數據, Android 開發者網站上文章Security with HTTPS and SSL做了詳盡的闡述.
OkHttp 自身是支持 Https 的. 參考文檔 OkHttp Https, 直接使用上面的 OkHttpStack就可以了, 但是如果遇到服務器開發哥哥使用了自簽名的證書(不要問我為什么要用自簽名的), 就無法正常訪問了.
網上有很多文章給出的方案是提供一個什么事情都不做的TrustManager 跳過 SSL 的驗證, 這樣做很容受到攻擊, Https 也就形同虛設了.
我采用的方案是將自簽名的證書打包入 APK 加入信任.
好處:
- 應用難以逆向, 應用不再依賴系統的 trust store, 使得 Charles 抓包等工具失效. 要分析應用 API 必須反編譯 APK.
- 不用額外購買證書, 省錢....
缺點:
- 證書部署靈活性降低, 一旦變更證書必須升級程序.
實現步驟
以最著名的自簽名網站12306為例說明
-
導出證書
- echo | openssl s_client -connect kyfw.12306.cn:443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > kyfw.12306.cn.pem
-
將證書轉為 bks 格式
下載最新的bcprov-jdk, 執行下面的命令. storepass 是導出密鑰文件的密碼.- keytool -importcert -v \
- -trustcacerts \
- -alias 0 \
- -file <(openssl x509 -in kyfw.12306.cn.pem) \
- -keystore $CERTSTORE -storetype BKS \
- -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
- -providerpath ./bcprov-jdk16-1.46.jar \
- -storepass asdfqaz
-
將導出的 kyfw.bks 文件放入 res/raw 文件夾下.
-
創建 SelfSignSslOkHttpStack
- /**
- * A HttpStack implement witch can verify specified self-signed certification.
- */
- public class SelfSignSslOkHttpStack extends HurlStack {
- private OkHttpClient okHttpClient;
- private Map<String, SSLSocketFactory> socketFactoryMap;
- /**
- * Create a OkHttpStack with default OkHttpClient.
- */
- public SelfSignSslOkHttpStack(Map<String, SSLSocketFactory> factoryMap) {
- this(new OkHttpClient(), factoryMap);
- }
- /**
- * Create a OkHttpStack with a custom OkHttpClient
- * @param okHttpClient Custom OkHttpClient, NonNull
- */
- public SelfSignSslOkHttpStack(OkHttpClient okHttpClient, Map<String, SSLSocketFactory> factoryMap) {
- this.okHttpClient = okHttpClient;
- this.socketFactoryMap = factoryMap;
- }
- @Override
- protected HttpURLConnection createConnection(URL url) throws IOException {
- if ("https".equals(url.getProtocol()) && socketFactoryMap.containsKey(url.getHost())) {
- HttpsURLConnection connection = (HttpsURLConnection) new OkUrlFactory(okHttpClient).open(url);
- connection.setSSLSocketFactory(socketFactoryMap.get(url.getHost()));
- return connection;
- } else {
- return new OkUrlFactory(okHttpClient).open(url);
- }
- }
- }
-
然后用 SelfSignSslOkHttpStack 創建 Volley 的 RequestQueue.
- String[] hosts = {"kyfw.12306.cn"};
- int[] certRes = {R.raw.kyfw};
- String[] certPass = {"asdfqaz"};
- socketFactoryMap = new Hashtable<>(hosts.length);
- for (int i = 0; i < certRes.length; i++) {
- int res = certRes[i];
- String password = certPass[i];
- SSLSocketFactory sslSocketFactory = createSSLSocketFactory(context, res, password);
- socketFactoryMap.put(hosts[i], sslSocketFactory);
- }
- HurlStack stack = new SelfSignSslOkHttpStack(socketFactoryMap);
- requestQueue = Volley.newRequestQueue(context, stack);
- requestQueue.start();
-
我們來試一試, 用上一步穿件的 RequestQueue 替換掉原來的, 然后發請求試試.
- StringRequest request = new StringRequest(
- Request.Method.GET,
- "https://kyfw.12306.cn/otn/",
- new Response.Listener<String>() {
- @Override
- public void onResponse(String response) {
- responseContentTextView.setText(response);
- }
- },
- new Response.ErrorListener() {
- @Override
- public void onErrorResponse(VolleyError error) {
- responseContentTextView.setText(error.toString());
- }
- });
- RequestManager.getInstance(this).addRequest(request, this);
-
done