Android 項目https網(wǎng)絡(luò)請求封裝及遇到的一些問題

因為最近ios需要用到https童擎,所以公司的項目都從http的請求轉(zhuǎn)成了https的雙向認(rèn)證捆蜀,這里我關(guān)于安卓端https相關(guān)的知識點以及在請求過程中遇到的一些問題当宴。

Paste_Image.png
一、https的介紹以及相關(guān)的專有名詞
https

簡單來說步清,HTTPS就是“安全版”的HTTP, HTTPS = HTTP + SSL。HTTPS相當(dāng)于在應(yīng)用層和TCP層之間加入了一個SSL(或TLS)虏肾,SSL層對從應(yīng)用層收到的數(shù)據(jù)進行加密廓啊。TLS/SSL中使用了RSA非對稱加密,對稱加密以及HASH算法封豪。

HTTPS和HTTP的區(qū)別

https協(xié)議需要到CA申請證書谴轮。
http是超文本傳輸協(xié)議,信息是明文傳輸吹埠;https 則是具有安全性的ssl加密傳輸協(xié)議第步。
http和https使用的是完全不同的連接方式,用的端口也不一樣缘琅,前者是80粘都,后者是443。
http的連接很簡單胯杭,是無狀態(tài)的驯杜;HTTPS協(xié)議是由SSL+HTTP協(xié)議構(gòu)建的可進行加密傳輸、身份認(rèn)證的網(wǎng)絡(luò)協(xié)議做个,比http協(xié)議安全鸽心。

http默認(rèn)使用80端口,https默認(rèn)使用443端口
Https的劣勢

對數(shù)據(jù)進行加解密決定了它比http慢需要進行非對稱的加解密居暖,且需要三次握手顽频,首次連接比較慢點。

CA證書:

證書授權(quán)中心簽發(fā)的證書太闺,在訪問這類網(wǎng)站的時候糯景,瀏覽器地址欄的左端一般都有一個綠色盾牌。

自簽名證書:

Android應(yīng)用程序開發(fā)最可能使用到,一般由服務(wù)器管理者生成蟀淮。平常大家使用自簽名證書的情況比較多最住,因為自簽名證書不僅免費,而且有效時間可以比較長怠惶。

TLS:(Transport Layer Security涨缚,傳輸層安全協(xié)議)

用于兩個應(yīng)用程序之間提供保密性和數(shù)據(jù)完整性。TLS 1.0是IETF(Internet Engineering Task Force策治,Internet工程任務(wù)組)制定的一種新的協(xié)議脓魏,它建立在SSL 3.0協(xié)議規(guī)范之上,是SSL 3.0的后續(xù)版本通惫,可以理解為SSL 3.1茂翔,它是寫入了 RFC的。

該協(xié)議由兩層組成: TLS 記錄協(xié)議(TLS Record)和 TLS 握手協(xié)議(TLS Handshake)履腋。
SSL(Secure Socket Layer珊燎,安全套接字層)

為Netscape所研發(fā),用以保障在Internet上數(shù)據(jù)傳輸之安全府树,利用數(shù)據(jù)加密(Encryption)技術(shù)俐末,可確保數(shù)據(jù)在網(wǎng)絡(luò)上之傳輸過程中不會被截取。它已被廣泛地用于Web瀏覽器與服務(wù)器之間的身份認(rèn)證和加密數(shù)據(jù)傳輸奄侠。SSL協(xié)議位于TCP/IP協(xié)議與各種應(yīng)用層協(xié)議之間卓箫,為數(shù)據(jù)通訊提供安全支持。

SSL協(xié)議可分為兩層:

1)SSL記錄協(xié)議(SSL Record Protocol):它建立在可靠的傳輸協(xié)議(如TCP)之上垄潮,為高層協(xié)議提供數(shù)據(jù)封裝烹卒、壓縮、加密等基本功能的支持弯洗。
2)SSL握手協(xié)議(SSL Handshake Protocol):它建立在SSL記錄協(xié)議之上旅急,用于在實際的數(shù)據(jù)傳輸開始前,通訊雙方進行身份認(rèn)證牡整、協(xié)商加密算法藐吮、交換加密密鑰等。

Paste_Image.png
二逃贝、https的單雙向認(rèn)證

服務(wù)器證書需要配置CA證書谣辞,不能是自簽名證書,甚至不能是不知名的證書機構(gòu)簽發(fā)的證書沐扳,否則就會有異常泥从。先說說https的認(rèn)證原理,如圖

Paste_Image.png
相關(guān)格式說明
JKS:數(shù)字證書庫沪摄。

JKS里有KeyEntry和CertEntry躯嫉,在庫里的每個Entry都是靠別名(alias)來識別的纱烘。

P12:是PKCS12的縮寫。

同樣是一個存儲私鑰的證書庫祈餐,由.jks文件導(dǎo)出的擂啥,用戶在PC平臺安裝,用于標(biāo)示用戶的身份帆阳。

CER:俗稱數(shù)字證書啤它。

目的就是用于存儲公鑰證書,任何人都可以獲取這個文件 舱痘。

BKS:

由于Android平臺不識別。keystore和.jks格式的證書庫文件离赫,因此Android平臺引入一種的證書庫格式芭逝,BKS。

有些人可能有疑問渊胸,為什么Tomcat只有一個server.keystore文件旬盯,而客戶端需要兩個庫文件?

因為有時客戶端可能需要訪問過個服務(wù)翎猛,而服務(wù)器的證書都不相同胖翰,因此客戶端需要制作一個truststore來存儲受信任的服務(wù)器的證書列表。因此為了規(guī)范創(chuàng)建一個truststore.jks用于存儲受信任的服務(wù)器證書切厘,創(chuàng)建一個client.jks來存儲客戶端自己的私鑰萨咳。對于只涉及與一個服務(wù)端進行雙向認(rèn)證的應(yīng)用,將server.cer導(dǎo)入到client.jks中也可疫稿。

1 單向認(rèn)證

如果你的項目的網(wǎng)絡(luò)框架是okhttp培他,那么使用https還是挺簡單的,因為okhttp默認(rèn)支持HTTPS遗座。

/**
     * HttpsUrlConnection 方式舀凛,支持指定**.crt證書驗證,此種方式Android官方建議
     * 
     * @throws CertificateException
     * @throws IOException
     * @throws KeyStoreException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     * @throws NoSuchProviderException
     */
    public void initHttpsConnection() {
        try {
            InputStream in = getAssets().open("server.cer");
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            Certificate  cer = certificateFactory.generateCertificate(in);
            System.out.println("ca=" + ((X509Certificate) cer).getSubjectDN());

            // Create a KeyStore containing our trusted CAs
            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            keystore.load(null);
            keystore.setCertificateEntry("ca", cer);

            // Create a TrustManager that trusts the CAs in our KeyStore
            String algorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
            trustManagerFactory.init(keystore);

            // Create an SSLContext that uses our TrustManager
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

            URL url = new URL(URL);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setSSLSocketFactory(sslContext.getSocketFactory());
            conn.setHostnameVerifier(DO_NOT_VERIFY);
            conn.setRequestMethod("POST");// 設(shè)置請求類型為post
            conn.setConnectTimeout(10000);// 設(shè)置超時時間
            conn.setReadTimeout(50000);
            conn.setDoInput(true);
            conn.setDoOutput(true);
            InputStream input = conn.getInputStream();

            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
            StringBuffer result = new StringBuffer();
            String line;
            while ((line = reader.readLine()) != null) {
                result.append(line);
                Log.d("TAG", line);
            }

        } catch (Exception e) {
            Log.e(this.getClass().getName(), e.getMessage());
        }
    }

    static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
        // 信任所有主機
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    };
三途蒋、谷歌官網(wǎng) HttpsURLConnection 信任自簽名證書代碼示例
Paste_Image.png
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStreamor ...)
// X.509 是Android唯一支持的證書格式
CertificateFactory cf =CertificateFactory.getInstance("X.509");
// From[https://www.washington.edu/itconnect/security/ca/load-der.crt](https://www.washington.edu/itconnect/security/ca/load-der.crt)
InputStream caInput = newBufferedInputStream(new FileInputStream("load-der.crt"));
// 證書的加載也可以是字符串的方式猛遍,建議使用字符串,并且對字符串做些額外的處理
// InputStream caInput=newByteArrayInputStream(cerString.getBytes());

Certificate ca;
try {      
      ca =cf.generateCertificate(caInput);
      System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} finally {
      caInput.close();
}

// Create a KeyStore containing our trusted CAs
// KeyStore 默認(rèn)類型是 BKS号坡,雖然 Android 的文檔中的例子寫了 JKS懊烤,但是 Android 并不支持JKS
String keyStoreType =KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null); 
// 加載一個默認(rèn)的秘鑰倉庫,倉庫是空的
keyStore.setCertificateEntry("ca",ca);

// Create a TrustManager that trusts the CAs inour KeyStore
// TrustManager 是證書校驗的關(guān)鍵筋帖,不使用系統(tǒng)默認(rèn)校驗方式時奸晴,需要開發(fā)者自己實現(xiàn)接口,完成校驗代碼
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();// 默認(rèn)算法 PKIX
TrustManagerFactory tmf =TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

// Create an SSLContext that uses ourTrustManager
// Android 不僅支持 TLS日麸,還有 TLSv1.2 等寄啼,TLSv1.2需要 API levels 20+
// 更多選擇可以參考[https://developer.android.com/re ... /ssl/SSLSocket.html](https://developer.android.com/reference/javax/net/ssl/SSLSocket.html)
SSLContext context =SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(),null);

// Tell the URLConnection to use aSocketFactory from our SSLContext
URL url = newURL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =      (HttpsURLConnection)url.openConnection();
// 證書校驗的關(guān)鍵逮光,設(shè)置SSLSocketFactory
// 執(zhí)行TrustManagerImpl 中 checkServerTrusted 方法完成服務(wù)器證書校驗
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in =urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

以上代碼,可以解決證書信任問題墩划。但同時需要注意的是涕刚,這里是基于Android默認(rèn)的信任檢查來解決的。因為我們沒有對TrustManager做任何修改乙帮,如果仍然遇到證書校驗不通過的情況杜漠,則需要重新實現(xiàn)TrustManager,請用以下代碼代替“tmf.getTrustManagers()”:

TrustManager tm = new X509TrustManager() {
    @Override
    public X509Certificate[] getAcceptedIssuers() {return null; }
    @Override
    public void checkServerTrusted(X509Certificate[] chain,String authType) throwsCertificateException {}
    @Override
    public void checkClientTrusted(X509Certificate[] chain,String authType) throwsCertificateException {
       // 方法直接返回察净,將不對服務(wù)器證書做任何校驗
       // 確認(rèn)服務(wù)器端證書頒發(fā)者和代碼中的證書主體一致
       if (!chain[0].getIssuerDN().equals(cert.getSubjectDN())) { }
       // 確認(rèn)服務(wù)器端證書被代碼中證書公鑰簽名                   
       chain[0].verify(cert.getPublicKey());
       // 確認(rèn)服務(wù)器端證書沒有過期
       chain[0].checkValidity();
     }
};
context.init(null, new TrustManager[]{tm},null);
如何校驗域名

在HttpsURLConnection中校驗域名是比較簡單的驾茴,這里Google提供了為URLConnection替換驗證過程的例子:

// Create an HostnameVerifier that hardwiresthe expected hostname.
// Note that is different than the URL'shostname:
// example.com versus example.org
// 這里強調(diào)的是URL中的域名和證書中不一致,所以默認(rèn)的校驗不能通過HostnameVerifier hostnameVerifier = newHostnameVerifier() {
      @Override
       publicboolean verify(String hostname, SSLSession session) {
              // 這段代碼中 hostname 應(yīng)該是 example.org
              // 獲取默認(rèn)的 HostnameVerifier
              HostnameVerifier hv =   HttpsURLConnection.getDefaultHostnameVerifier();
              // 如果直接返回 true氢卡,等于不做域名校驗
              return hv.verify("example.com", session);
       }
};

// Tell the URLConnection to use ourHostnameVerifier
URL url = newURL("https://example.org/");
HttpsURLConnection urlConnection =(HttpsURLConnection)url.openConnection();
urlConnection.setHostnameVerifier(hostnameVerifier);
InputStream in =urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

正確使用域名校驗很重要锈至,直接返回true顯然不安全∫肭兀回調(diào)參數(shù)hostname是訪問的域名峡捡,session可以通過getPeerCertificates獲取到證書的相關(guān)信息,如果需要自己寫代碼完成域名校驗筑悴,需要根據(jù)實際開發(fā)情況正確校驗们拙。

總結(jié)

用HttpsURLConnection來校驗證書和域名的方法如下(不明確指定校驗方式時,系統(tǒng)會采用默認(rèn)的方式):

HttpsURLConnection urlConnection =(HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
urlConnection.setHostnameVerifier(hostnameVerifier);

校驗的順序是先校驗證書阁吝,再校驗域名砚婆。為了能夠更好的理解Android HTTPS證書校驗和域名校驗的默認(rèn)實現(xiàn),可以參考Android的源代碼:
證書校驗的默認(rèn)實現(xiàn):類:TrustManagerImpl.java

Git 獲取源碼:git clonehttps://android.googlesource.com/platform/external/conscrypt
域名校驗的默認(rèn)實現(xiàn):類:OkHostnameVerifier.java

Git 獲取源碼:git clonehttps://android.googlesource.com/platform/external/okhttp

四突勇、https證書的制作

很明顯要測https射沟,必須要有證書∮刖常客戶端持有服務(wù)端的公鑰證書验夯,并持有自己的私鑰,服務(wù)端持有客戶的公鑰證書摔刁,并持有自己私鑰挥转。

https認(rèn)證原理:

1)建立連接的時候,客戶端利用服務(wù)端的公鑰證書來驗證服務(wù)器是否上是目標(biāo)服務(wù)器共屈。
2)服務(wù)端利用客戶端的公鑰來驗證客戶端是否是目標(biāo)客戶端绑谣。(請參考RSA非對稱加密以及HASH校驗算法)
3)服務(wù)端給客戶端發(fā)送數(shù)據(jù)時,需要將服務(wù)端的證書發(fā)給客戶端驗證拗引,驗證通過才運行發(fā)送數(shù)據(jù)借宵。
4)同樣,客戶端請求服務(wù)器數(shù)據(jù)時矾削,也需要將自己的證書發(fā)給服務(wù)端驗證壤玫,通過才允許執(zhí)行請求豁护。

證書制作步驟如下:
1 生成客戶端keystore

keytool -genkeypair -alias client -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore client.jks

2 生成服務(wù)端keystore

keytool -genkeypair -alias server -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore server.keystore
//注意:CN必須與IP地址匹配,否則需要修改host

3 導(dǎo)出客戶端證書

keytool -export -alias client -file client.cer -keystore client.jks -storepass 123456

4 導(dǎo)出服務(wù)端證書

keytool -export -alias server -file server.cer -keystore server.keystore -storepass 123456

5 重點:證書交換

將客戶端證書導(dǎo)入服務(wù)端keystore中欲间,再將服務(wù)端證書導(dǎo)入客戶端keystore中楚里, 一個keystore可以導(dǎo)入多個證書,生成證書列表猎贴。

  • 生成客戶端信任證書庫(由服務(wù)端證書生成的證書庫):
    keytool -import -v -alias server -file server.cer -keystore truststore.jks -storepass 123456
  • 將客戶端證書導(dǎo)入到服務(wù)器證書庫(使得服務(wù)器信任客戶端證書):
    keytool -import -v -alias client -file client.cer -keystore server.keystore -storepass 123456
6 生成Android識別的BKS庫文件

用Portecle工具轉(zhuǎn)成bks格式班缎,最新版本是1.10。
下載鏈接:https://sourceforge.net/projects/portecle/
運行protecle.jar將client.jks和truststore.jks分別轉(zhuǎn)換成client.bks和truststore.bks,然后放到android客戶端的assert目錄下
 
File -> open Keystore File -> 選擇證書庫文件 -> 輸入密碼 -> Tools -> change keystore type -> BKS -> save keystore as -> 保存即可
 
這個操作很簡單她渴,如果不懂可自行百度达址。
 
我在Windows下生成BKS的時候會報錯失敗,后來我換到CentOS用OpenJDK1.7立馬成功了趁耗,如果在這步失敗的同學(xué)可以換到Linux或Mac下操作苏携,
將生成的BKS拷貝回Windows即可。

7 配置Tomcat服務(wù)器

修改server.xml文件对粪,配置8443端口
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="${catalina.base}/key/server.keystore" keystorePass="123456"
truststoreFile="${catalina.base}/key/server.keystore" truststorePass="123456"/>
 
備注: - keystoreFile:指定服務(wù)器密鑰庫,可以配置成絕對路徑装蓬,本例中是在Tomcat目錄中創(chuàng)建了一個名為key的文件夾著拭,僅供參考。
- keystorePass:密鑰庫生成時的密碼
- truststoreFile:受信任密鑰庫牍帚,和密鑰庫相同即可
- truststorePass:受信任密鑰庫密碼

8 Android App編寫B(tài)KS讀取創(chuàng)建證書自定義的SSLSocketFactory
private final static String CLIENT_PRI_KEY = "client.bks";
private final static String TRUSTSTORE_PUB_KEY = "truststore.bks";
private final static String CLIENT_BKS_PASSWORD = "123456";
private final static String TRUSTSTORE_BKS_PASSWORD = "123456";
private final static String KEYSTORE_TYPE = "BKS";
private final static String PROTOCOL_TYPE = "TLS";
private final static String CERTIFICATE_FORMAT = "X509";
 
public static SSLSocketFactory getSSLCertifcation(Context context) {
  SSLSocketFactory sslSocketFactory = null;
  try {
    // 服務(wù)器端需要驗證的客戶端證書儡遮,其實就是客戶端的keystore
    KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);// 客戶端信任的服務(wù)器端證書
    KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);//讀取證書
    InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
    InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);//加載證書
    keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
    trustStore.load(tsIn, TRUSTSTORE_BKS_PASSWORD.toCharArray());
    ksIn.close();
    tsIn.close();
    //初始化SSLContext
    SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE);
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_FORMAT);
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_FORMAT);
    trustManagerFactory.init(trustStore);
    keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
    sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 
 
    sslSocketFactory = sslContext.getSocketFactory();
 
  } catch (KeyStoreException e) {...}//省略各種異常處理,請自行添加
  return sslSocketFactory;
}
9 Android App獲取SSLFactory實例進行網(wǎng)絡(luò)訪問
private void fetchData() {
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
      .sslSocketFactory(SSLHelper.getSSLCertifcation(context))//獲取SSLSocketFactory
      .hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName驗證器
      .build();
 
  Retrofit retrofit = new Retrofit.Builder()
       .baseUrl("https://10.2.8.56:8443")//填寫自己服務(wù)器IP
       .addConverterFactory(GsonConverterFactory.create())//添加 json 轉(zhuǎn)換器
       .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 適配器
       .client(okHttpClient)
       .build();
 
  IUser userIntf = retrofit.create(IUser.class);
   
  userIntf.getUser(user.getPhone())
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread()) 
        .subscribe(new Subscriber<UserBean>() {
                //省略onCompleted暗赶、onError蹈丸、onNext
        }
  });
}
private class UnSafeHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(String hostname, SSLSession session) {
      return true;//自行添加判斷邏輯多柑,true->Safe,false->unsafe
  }
}
okttp設(shè)置證書的方法
private staticSSLSocketFactorygetSSLSocketFactory_Certificate(Context context, String keyStoreType,intkeystoreResId)

throwsCertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException {

CertificateFactory cf = CertificateFactory.getInstance("X.509");

InputStream caInput = context.getResources().openRawResource(keystoreResId);

Certificate ca = cf.generateCertificate(caInput);

caInput.close();

if(keyStoreType ==null|| keyStoreType.length() ==0) {

keyStoreType = KeyStore.getDefaultType();

}

KeyStore keyStore = KeyStore.getInstance(keyStoreType);

keyStore.load(null,null);

keyStore.setCertificateEntry("ca", ca);

String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();

TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);

tmf.init(keyStore);

TrustManager[] wrappedTrustManagers =getWrappedTrustManagers(tmf.getTrustManagers());

SSLContext sslContext = SSLContext.getInstance("TLS");

sslContext.init(null, wrappedTrustManagers,null);

returnsslContext.getSocketFactory();

}

調(diào)用時把服務(wù)器生成的.cer證書放到raw目錄下(其他地方自己搞),然后調(diào)用:

SSLSocketFactory sslSocketFactory =getSSLSocketFactory_Certificate(context,"BKS", R.raw.XXX);
//new一個OkHttpClient
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setSslSocketFactory(sslSocketFactory);

Retrofit構(gòu)造的adapter使用自定義okHttpClient

RestAdapter.Builder builder =newRestAdapter.Builder()

.setEndpoint(serverUrl)

.setClient(newOkClient(okHttpClient));
五蓝翰、https遇到的一些坑
1 Https網(wǎng)絡(luò)請求的時候經(jīng)常碰到handshake aborted。解決辦法如下
package com.guiying.common.http;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RawRes;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
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.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

/**
 * HttpsUtils來自于https://github.com/guiying712/AndroidModulePattern/blob/master/common/src/main/java/com/guiying/common/http/HttpsUtil.java圆米;
 * 其他參考的文章有:http://android.jobbole.com/83787/吊输;
 *
 * Android 4.X 對TLS1.1、TLS1.2的支持參考了http://blog.csdn.net/joye123/article/details/53888252
 */
public class HttpsUtil {

    /**
     * 包裝的 SSL(Secure Socket Layer)參數(shù)類
     */
    public static class SSLParams {
        public SSLSocketFactory sSLSocketFactory;
        public X509TrustManager trustManager;
    }

    /**
     * @param context        上下文
     * @param certificatesId "XXX.cer" 文件 (文件位置res/raw/XXX.cer)
     * @param bksFileId      "XXX.bks"文件(文件位置res/raw/XXX.bks)
     * @param password       The certificate's password.
     * @return SSLParams
     */
    public static SSLParams getSslSocketFactory(Context context, @RawRes int[] certificatesId, @RawRes int bksFileId, String password) {
        if (context == null) {
            throw new NullPointerException("context == null");
        }
        SSLParams sslParams = new SSLParams();
        try {
            TrustManager[] trustManagers = prepareTrustManager(context, certificatesId);
            KeyManager[] keyManagers = prepareKeyManager(context, bksFileId, password);

            //創(chuàng)建TLS類型的SSLContext對象激率,that uses our TrustManager
            SSLContext sslContext = SSLContext.getInstance("TLS");

            X509TrustManager x509TrustManager;
            if (trustManagers != null) {
                x509TrustManager = new MyTrustManager(chooseTrustManager(trustManagers));
            } else {
                x509TrustManager = new UnSafeTrustManager();
            }
            //用上面得到的trustManagers初始化SSLContext咳燕,這樣sslContext就會信任keyStore中的證書
            sslContext.init(keyManagers, new TrustManager[]{x509TrustManager}, null);

            //通過sslContext獲取SSLSocketFactory對象
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                /*Android 4.X 對TLS1.1、TLS1.2的支持*/
                sslParams.sSLSocketFactory = new Tls12SocketFactory(sslContext.getSocketFactory());
                sslParams.trustManager = x509TrustManager;
                return sslParams;
            }

            sslParams.sSLSocketFactory = sslContext.getSocketFactory();
            sslParams.trustManager = x509TrustManager;
            return sslParams;
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            throw new AssertionError(e);
        }
    }


    /**
     * 主機名校驗方法
     */
    public static HostnameVerifier getHostnameVerifier() {
        return new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return hostname.equalsIgnoreCase(session.getPeerHost());
            }
        };
    }


    private static TrustManager[] prepareTrustManager(Context context, int[] certificatesId) {
        if (certificatesId == null || certificatesId.length <= 0) {
            return null;
        }

        try {
            //創(chuàng)建X.509格式的CertificateFactory
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            // 創(chuàng)建一個默認(rèn)類型的KeyStore乒躺,存儲我們信任的證書
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (int certificateId : certificatesId) {
                //從本地資源中獲取證書的流
                InputStream cerInputStream = context.getResources().openRawResource(certificateId);
                String certificateAlias = Integer.toString(index++);

                //certificate是java.security.cert.Certificate招盲,而不是其他Certificate
                //證書工廠根據(jù)證書文件的流生成證書Certificate
                Certificate certificate = certificateFactory.generateCertificate(cerInputStream);
                //將證書certificate作為信任的證書放入到keyStore中
                keyStore.setCertificateEntry(certificateAlias, certificate);
                try {
                    if (cerInputStream != null)
                        cerInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            //TrustManagerFactory是用于生成TrustManager的,這里創(chuàng)建一個默認(rèn)類型的TrustManagerFactory
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            //用我們之前的keyStore實例初始化TrustManagerFactory,這樣trustManagerFactory就會信任keyStore中的證書
            trustManagerFactory.init(keyStore);
            return trustManagerFactory.getTrustManagers();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    private static KeyManager[] prepareKeyManager(Context context, @RawRes int bksFileId, String password) {

        try {
            KeyStore clientKeyStore = KeyStore.getInstance("BKS");
            clientKeyStore.load(context.getResources().openRawResource(bksFileId), password.toCharArray());
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(clientKeyStore, password.toCharArray());
            return keyManagerFactory.getKeyManagers();

        } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException | IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) {
        for (TrustManager trustManager : trustManagers) {
            if (trustManager instanceof X509TrustManager) {
                return (X509TrustManager) trustManager;
            }
        }
        return null;
    }


    /**
     * 客戶端不對證書做任何檢查;
     * 客戶端不對證書做任何驗證的做法有很大的安全漏洞嘉冒。
     */
    private static class UnSafeTrustManager implements X509TrustManager {

        @SuppressLint("TrustAllX509TrustManager")
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
        }

        @SuppressLint("TrustAllX509TrustManager")
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[]{};
        }
    }


    private static class MyTrustManager implements X509TrustManager {
        private X509TrustManager defaultTrustManager;
        private X509TrustManager localTrustManager;

        private MyTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException {
            //TrustManagerFactory是用于生成TrustManager的,創(chuàng)建一個默認(rèn)類型的TrustManagerFactory
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init((KeyStore) null);
            defaultTrustManager = chooseTrustManager(trustManagerFactory.getTrustManagers());
            this.localTrustManager = localTrustManager;
        }


        @SuppressLint("TrustAllX509TrustManager")
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                defaultTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException ce) {
                localTrustManager.checkServerTrusted(chain, authType);
            }
        }


        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }


    /**
     * 自行實現(xiàn)SSLSocketFactory 曹货,實現(xiàn)Android 4.X 對TLSv1.1咆繁、TLSv1.2的支持
     */
    private static class Tls12SocketFactory extends SSLSocketFactory {

        private static final String[] TLS_SUPPORT_VERSION = {"TLSv1.1", "TLSv1.2"};

        final SSLSocketFactory delegate;

        private Tls12SocketFactory(SSLSocketFactory base) {
            this.delegate = base;
        }

        @Override
        public String[] getDefaultCipherSuites() {
            return delegate.getDefaultCipherSuites();
        }

        @Override
        public String[] getSupportedCipherSuites() {
            return delegate.getSupportedCipherSuites();
        }

        @Override
        public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
            return patch(delegate.createSocket(s, host, port, autoClose));
        }

        @Override
        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
            return patch(delegate.createSocket(host, port));
        }

        @Override
        public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
            return patch(delegate.createSocket(host, port, localHost, localPort));
        }

        @Override
        public Socket createSocket(InetAddress host, int port) throws IOException {
            return patch(delegate.createSocket(host, port));
        }

        @Override
        public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
            return patch(delegate.createSocket(address, port, localAddress, localPort));
        }

        private Socket patch(Socket s) {
            //代理SSLSocketFactory在創(chuàng)建一個Socket連接的時候,會設(shè)置Socket的可用的TLS版本控乾。
            if (s instanceof SSLSocket) {
                ((SSLSocket) s).setEnabledProtocols(TLS_SUPPORT_VERSION);
            }
            return s;
        }
    }


}
2 SSLHandshakeException

導(dǎo)致SSLHandshakeException是由于簽署證書的CA不被系統(tǒng)所信任么介。一種原因是CA機構(gòu)比較新,還沒被android系統(tǒng)證書庫內(nèi)置蜕衡∪蓝蹋或者你的android版本比較舊,證書庫不全慨仿。
可能有以下幾個原因:

1.簽名證書的CA機構(gòu)不知名
2.使用了自簽名的證書
3.服務(wù)端配置問題

這種情況的解決方案和使用了自簽名證書的方案是一樣的久脯,重寫Google默認(rèn)的證書校驗邏輯。

六镰吆、最后

雖然https算是加密成功了帘撰,但是客戶端里面放私鑰并不安全。 很容易被拿出來. 所以銀行都用u盾解決万皿。網(wǎng)上有一些關(guān)于客戶端安全性的說法摧找,粘出來給大家看下

  1. 手動輸入密碼. 如社交類, 電商
  1. 通過jni, 花指令方式, 或者經(jīng)過一系列復(fù)雜運算繞幾圈得出密鑰---增加破解難度
  2. 銀行那種用硬件。
  3. 加強服務(wù)器接口的安全性, 能保證接口泄露了也不會造成大的危害. 極端點甚至可以公開接口鼓勵第三方實現(xiàn).
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牢硅,一起剝皮案震驚了整個濱河市蹬耘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌减余,老刑警劉巖综苔,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異位岔,居然都是意外死亡如筛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門抒抬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杨刨,“玉大人,你說我怎么就攤上這事擦剑∈眉蓿” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵抓于,是天一觀的道長做粤。 經(jīng)常有香客問我,道長捉撮,這世上最難降的妖魔是什么怕品? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮巾遭,結(jié)果婚禮上肉康,老公的妹妹穿的比我還像新娘闯估。我一直安慰自己,他們只是感情好吼和,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布涨薪。 她就那樣靜靜地躺著,像睡著了一般炫乓。 火紅的嫁衣襯著肌膚如雪刚夺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天末捣,我揣著相機與錄音侠姑,去河邊找鬼。 笑死箩做,一個胖子當(dāng)著我的面吹牛莽红,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播邦邦,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼安吁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了燃辖?” 一聲冷哼從身側(cè)響起鬼店,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎郭赐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體确沸,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡捌锭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罗捎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片观谦。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖桨菜,靈堂內(nèi)的尸體忽然破棺而出豁状,到底是詐尸還是另有隱情,我是刑警寧澤倒得,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布泻红,位于F島的核電站,受9級特大地震影響霞掺,放射性物質(zhì)發(fā)生泄漏谊路。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一菩彬、第九天 我趴在偏房一處隱蔽的房頂上張望缠劝。 院中可真熱鬧潮梯,春花似錦、人聲如沸惨恭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脱羡。三九已至萝究,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間轻黑,已是汗流浹背糊肤。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留氓鄙,地道東北人馆揉。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像抖拦,于是被迫代替她去往敵國和親升酣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 由于最近要做一個安全性比較高的項目态罪,因此需要用到HTTPS進行雙向認(rèn)證噩茄。由于設(shè)計項目架構(gòu)的時候,客戶端是采用MVV...
    ChongmingLiu閱讀 33,352評論 61 209
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理复颈,服務(wù)發(fā)現(xiàn)绩聘,斷路器,智...
    卡卡羅2017閱讀 134,601評論 18 139
  • 目錄 準(zhǔn)備 分析2.1. 三次握手2.2. 創(chuàng)建 HTTP 代理(非必要)2.3. TLS/SSL 握手2.4. ...
    RunAlgorithm閱讀 37,894評論 12 117
  • 原文地址 http://blog.csdn.net/u012409247/article/details/4985...
    0fbf551ff6fb閱讀 3,514評論 0 13
  • 學(xué)校今天舉行五年級畢業(yè)典禮耗啦,小哥小學(xué)畢業(yè)了凿菩。 還能記得一年級升小學(xué)的開學(xué)典禮,學(xué)校老師講了一個媽媽不放棄帶著孩子一...
    花手鞠閱讀 192評論 0 2