Android 偶遇 HTTPS
HTTPS ,該來的總要來的。
最近領導對移動端開發提出了很多優化的要求啊!其中一點就是數據安全性,之前安卓后端接口一直是用的HTTP,那么我想了想,HTTPS應該是入門級的了,趕緊找資料整理了下!
對于向權威機構申請過證書的網絡地址,用 OkHttp 或者 HttpsURLConnection 都可以直接訪問,不需要做額外的事情。但是申請證書要$$的,所以開發的時候我們接口經常是使用自簽名證書,或者即使上線了也還是用自簽名的,因為安卓用到的基本都是數據接口,又不會用瀏覽器訪問,不想付錢不行咩!
訪問自簽名網址
使用keytool生成證書
keytool是JDK提供的管理加密密鑰、X.509證書鏈和可信證書密鑰庫的簡便工具。安卓開發必定安裝了JDK并且一般都會配置好環境變量,所以你可以直接在終端或DOC窗口輸入 keytool 命令來查看幫助。
1.生成密鑰對
keytool -genkey -alias server -keyalg RSA -keystore server.jks
-alias 后面跟的是唯一別名, -keystore 后面填保存秘鑰對的文件路徑
還可以添加一個 -validity 天數 聲明有效期
需要注意的地方:執行命令之后第一個問題讓你輸入名字的地方最好設置成 域名 ,比如這樣 baidu.com 或者這樣 localhost ,反正匹配你要調式的域名就對了,當然,如果你在安卓上調試,那么本地地址可能用不了。
2.導出證書
上面生成了服務端使用的密鑰對,現在可以通過它生成證書給客戶端使用
keytool -export -alias server -storepass 123456 -keystore server.jks -file server.cer
-storepass 后面跟的是你剛才設置的密碼,不加這個也沒關系,它會主動問你!; -file 設置了保存證書的路徑
服務端配置
這里我使用tomcat8進行測試,它的配置很簡單,修改tomcat目錄下的conf/server.xml文件,添加如下內容,這里設置了端口號為8443
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS"
keystoreFile="密鑰庫文件路徑,也就是.jks文件"
keystorePass="密碼" />
安卓端配置
- 加載證書
把之前生成的證書(.cer)放到安卓項目的 assets 或者 raw 目錄下,讀取文件流用以下方法獲取SSLSocketFactory 。
public static SSLSocketFactory getSslSocketFactory(InputStream certificates)
{
SSLContext sslContext = null;
try
{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate ca;
try {
ca = certificateFactory.generateCertificate(certificates);
} finally {
certificates.close();
}
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
} catch (Exception e)
{
e.printStackTrace();
}
return sslContext != null? sslContext.getSocketFactory():null;
}
OkHttp
在OkHttp中使用很簡單,獲取SSLSocketFactory之后通過OkHttp的構建方法傳入就行了。
使用的OkHttp版本是 3.2.0 。
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory)
.build();
設置完之后你就可以訪問該證書對應的域名地址了,不需要別的附加操作了。
HttpsURLConnection
OkHttp的API與安卓中默認提供的URLConnection是很接近的,所以配置也是如出一轍。
URL url = new URL("https://....");
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
雙向驗證
雙向認證需要兩個密鑰實體,一個放服務端一個放客戶端。前面我們已經實現單向的認證,現在只需要給客戶端生成一個密鑰庫,并且讓服務端信任客戶端就可以了。
生成客戶端密鑰
keytool -genkey -alias android -keyalg RSA -keystore android.jks
導出客戶端證書(字符串形式)
keytool -keystore android.jks -alias android -exportcert -rfc > android.pem
將導出的證書添加信任到服務端的密鑰庫
keytool -importcert -trustcacerts -alias android -keystore server.jks -file android.pem
服務端配置
修改tomcat目錄下的conf/server.xml文件
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
sslProtocol="TLS"
keystoreFile="密鑰庫文件路徑,也就是.jks文件"
keystorePass="密碼"
//修改兩條內容,其它和之前單向認證一樣就行
clientAuth="true"
truststoreFile="和keystoreFile填一樣" />
安卓端配置
剛才生成了客戶端的密鑰庫 android.jks 。但是安卓默認是不支持jks格式的!比較常規的解決方式是用 Portecle 工具將它轉換成bks文件。
下載完之后解壓并在目錄下運行命令: java -jar portecle.jar 或者也可以直接雙擊它打開
運行之后就會出來UI界面,用它打開 android.jks 然后選菜單 Tools –> Change Keystore Type –> BKS 在彈出框輸入密碼進行轉換,最后別忘記選菜單 File –> Save Keystore As 將它另存為 android.kbs (名字隨意)
可能會出現這樣的異常:java.security.KeyStoreException: java.io.IOException: Error initialising store of key store
解決辦法是下載 JCE 然后替換掉 JDK\jre\lib\security 和 JRE\lib\security 這兩個目錄下的同名文件,并重啟Portecle
生成kbs文件之后,把它放到安卓的目錄下 assets 或者 raw 。
然后把獲取SSLSocketFactory的方法改成下面這樣
public static SSLSocketFactory getSslSocketFactory
(InputStream certificates,InputStream key,String keyPassword)
{
SSLContext sslContext = null;
try
{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate ca;
try {
ca = certificateFactory.generateCertificate(certificates);
} finally {
certificates.close();
}
String 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);
String keyStoreType2 = "BKS";
KeyStore keyStore2 = KeyStore.getInstance(keyStoreType2);
keyStore2.load(key, keyPassword.toCharArray());
String kmfAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm);
kmf.init(keyStore2,keyPassword.toCharArray());
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
} catch (Exception e)
{
e.printStackTrace();
}
return sslContext != null? sslContext.getSocketFactory():null;
}
上面的都改完之后,雙向驗證的配置就完成了。你可以打開瀏覽器訪問下你配置好的地址,應該不能訪問,提示你: 不接受您的登錄證書,或者您的登錄證書可能已過期 。因為你的系統沒有加入剛才生成的客戶端密鑰庫,安卓端像上面一樣設置完SSLSocketFactory就可以正常訪問了。
額外的,我把獲取SSLSocketFactory的方法封裝了下 HttpsUtil ,用 getSslSocketFactory 方法就行了