為什么需要雙向認證
Https保證的是信道的安全,即客戶端和服務端通信報文的安全。但是無法保證中間人攻擊,所以雙向認證解決的問題就是防止中間人攻擊。
中間人攻擊(Man-in-the-MiddleAttack)簡稱(MITM),是一種“間接”的入侵攻擊,這種攻擊模式是通過各種技術手段將受入侵者控制的一臺計算機虛擬放置在網絡連接中的兩臺通信計算機之間,這臺計算機就稱為“中間人”。若沒有開啟雙向認證,中間人可以攔截客戶端發送的請求,然后篡改信息再發送到服務端;中間人也可以攔截服務端返回的信息,再發送到客戶端。所以使用Https的單向認證或雙向認證能夠有效防止中間人攻擊。
注:無論Ca證書還是自簽證書都需要雙向認證。
雙向認證原理
1、服務端認證客戶端原理
客戶端有自己的bks證書auth_client.bks,并將導出的auth_client_pub.cer證書導入到服務端證書auth_server.keystore中,這樣服務端就將客戶端證書添加到信任列表中,從而能夠讓帶有該auth_client_pub.cer證書信息的客戶端訪問服務。
2、客戶端認證服務端原理
服務端有自己的證書(ca頒發的或者是自己創建的)auth_server.keystore,并導出auth_server_pub.cer證書,將該證書導入到客戶端證書
auth_truststore.jks中,注意:這里不是導入到auth_client.jks中,而是導入生成另一個證書auth_truststore.jks,最后再將jks證書轉化成bks證書。
實現過程
一、服務端證書
創建服務端證書
keytool -genkeypair -alias auth_server -keyalg RSA -validity 36500 -keypass auth_server -storepass auth_server -keystore /Users/renzhongrui/android/certs/auth_server.keystore
導出服務端證書公鑰
keytool -export -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server
二、客戶端證書
創建客戶端證書(andoird不能用keystore格式的密鑰庫,所以先生成jks格式,再用Portecle工具轉成bks格式)
keytool -genkeypair -alias auth_client -keyalg RSA -validity 36500 -keypass auth_client -storepass auth_client -keystore /Users/renzhongrui/android/certs/auth_client.jks
導出客戶端證書公鑰
keytool -export -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_client.jks -storepass auth_client
三、證書交換
將客戶端證書導入服務端keystore中,再將服務端證書導入客戶端auth_truststore中, 一個keystore可以導入多個證書,生成證書列表。
將客戶端公鑰導入到服務端keystore證書中,使得服務端能夠信任客戶端。
keytool -import -v -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server
生成客戶端信任證書庫auth_truststore.jks,即將服務端公鑰導入到客戶端jks證書中,使得客戶端能夠信任服務端。
keytool -import -v -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore
最后驗證一下,查看證書庫中的所有證書
keytool -list -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server
keytool -list -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore
四、證書轉換
下載portecle.jar(https://sourceforge.net/projects/portecle/),解壓后運行jar包:-
java -jar portecle.jar
1、點擊File菜單選擇Open Keystore File,選擇創建好的auth_client.jks或auth_truststore.jks證書,輸入密碼。 2、選中導入的證書,點擊Tools菜單,選擇Change Keystore Type,再選擇BKS類型,再次輸入密碼,確認之后,會顯示成功。 3、最后點擊File菜單,選擇Save Keystore File As,將證書保存的指定路徑。
五、配置服務
證書準備好之后,就可以進行集成測試了,服務使用Spring Boot創建或者使用Nginx代理。
使用Spring Boot服務
1、添加配置
server:
port: 443
server:
tomcat:
uri-encoding: UTF-8
# 開啟https,配置跟證書對應
ssl:
key-store: classpath:auth_server.keystore
key-store-type: JKS
key-store-password: auth_server
key-password: auth_server
key-alias: auth_server
enabled: true
#是否需要進行認證
client-auth: need
protocol: TLS # 默認
trust-store: classpath:auth_server.keystore
trust-store-password: auth_server
trust-store-type: JKS
2、添加代碼,這里配置80端口重定向到443,也可以改成別的端口。
public class PackApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(PackApplication.class, args);
}
@Bean
public Connector connector(){
Connector connector=new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(80);
connector.setSecure(false);
connector.setRedirectPort(443);
return connector;
}
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector){
TomcatServletWebServerFactory tomcat=new TomcatServletWebServerFactory(){
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint=new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection=new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(connector);
return tomcat;
}
}
使用Nginx服務配置
Nginx配置與Spring Boot服務配置略有不同。
server {
listen 443;
server_name 192.168.200.101; # 代理服務IP
ssl on; # 開啟Https
ssl_certificate /usr/local/nginx/conf/https/auth_server.cer; # auth_server.keystore導出的cer證書
ssl_certificate_key /usr/local/nginx/conf/https/auth_server.key; # auth_server.keystore導出的私鑰
ssl_client_certificate /usr/local/nginx/conf/https/auth_client.cer; # auth_client.keystore導出的cer
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_verify_client optional; # 配置校驗客戶端策略,設置成optional時候可以開啟白名單接口
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers off;
location / { # 需要雙向驗證https的接口
if ($ssl_client_verify != SUCCESS) {
return 401;
}
proxy_pass http://192.168.200.101:8008;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
location /aarm/downloadUpdateFile { # 獲取證書版本和下載證書接口,不需要驗證Https
proxy_pass http://192.169.200.101:8008;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
六、配置客戶端
在客戶端app中使用OkHttp來進行網絡訪問,所以需要配置OkHttp來進行證書認證。
1、將上面創建的auth_client.bks和auth_truststore.bks證書放到assets目錄下 2、初始化OkHttp
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.sslSocketFactory(Https.getSSLCertifcation(getApplicationContext()))//獲取SSLSocketFactory
.hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName驗證器
.build();
重點需要看一下Https類的實現:
public class Https {
private final static String CLIENT_PRI_KEY = "auth_client.bks";
private final static String TRUSTSTORE_PUB_KEY = "auth_truststore.bks";
private final static String CLIENT_BKS_PASSWORD = "auth_client";
private final static String TRUSTSTORE_BKS_PASSWORD = "auth_truststore";
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 {
// 服務器端需要驗證的客戶端證書,其實就是客戶端的keystore
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);// 客戶端信任的服務器端證書
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 (Exception e) {
e.printStackTrace();
}
return sslSocketFactory;
}
}
還有一個UnSafeHostnameVerifier類
private class UnSafeHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
此時再進行網絡請求,就能夠訪問到帶有雙向認證的服務端接口了。當然一般網上的博客到這就結束了,但是這樣真的就完事了嗎,其實真正的設計才剛開始,如果只是了解原理,讀到這里就可以了,下面才是真實應用場景。
真實場景實現
原理還是那個原理,就看怎么合理的使用了。在真實開發環境中,需要解決幾個問題: auth_client.bks和auth_truststore.bks是需要動態下發的不是所有的接口都需要進行雙向認證
動態下發auth_client.bks和auth_truststore.bks
1、auth_client.bks和auth_truststore.bks的制作需要在本地工具完成,然后通過管理端上傳到服務器,并且改變證書的版本號; 2、客戶端需要訪問證書版本,來判斷是否需要更新證書,如果需要更新則下載證書。
這里會引出兩個問題:
1、請求版本號的接口和下載證書的接口不能進行雙向認證,否則無法下發證書。 2、不進行雙向認證的接口是不安全的,所以,請求版本號的接口的返回值是需要加密的;
針對第一個問題處理方式:
服務端需要配置白名單,將請求版本號的接口和下載證書的接口過濾掉;
客戶端OkHttp首次初始化不能進行雙向認證,等下載完證書之后,需要再次進行OkHttp初始化;
針對第二個問題處理方式:
需要本地工具創建RSA公私鑰對,用于請求版本號接口的加解密;
服務端使用私鑰對報文加密,客戶端保存公鑰,并使用公鑰對報文解密。
客戶端使用公鑰解密后的報文格式:
{
"version":1,
"authType":2,
"clientBksPath":"https://localhost/downloadUpdateFile?fileName=auth_client.bks",
"trustBksPath":"https://localhost/downloadUpdateFile?fileName=auth_truststore.bks",
"authKey":"auth_client"
}
version: 表示每一次更換證書的版本;
authType:0 表示不開啟認證,1 表示開啟單向認證,2 表示開啟雙向認證clientBksPath:auth_client.bks下載路徑trustBksPath:auth_truststore.bks下載路徑authKey:auth_client.bks證書密碼客戶端每次啟動都要獲取服務端證書版本,并將證書信息存儲到本地文件或者數據庫中,通過對比服務端證書版本和數據庫中版本來判斷是否需要證書更新。
注:這樣設計的好處是當證書過期時,能夠動態下發證書,但會引出一個問題,客戶端要安全的存儲公鑰信息,一般做法是將公鑰存儲到so文件里,再配合應用加固手段進行保護,不過這個就不是通信安全的問題了,而是apk安全的問題。
其他證書操作
1、查看keystore證書公鑰-
keytool -list -rfc --keystore release.keystore | openssl x509 -inform pem -pubkey
2、查看keystore證書私鑰
先轉成pfx格式
keytool -v -importkeystore -srckeystore release.keystore -srcstoretype jks -srcstorepass 123456 -destkeystore keystore/release.pfx -deststoretype pkcs12 -deststorepass 123456 -destkeypass 123456
再查看證書私鑰
openssl pkcs12 -in release.pfx -nocerts -nodes