為什么需要雙向認(rèn)證
Https保證的是信道的安全,即客戶端和服務(wù)端通信報文的安全。但是無法保證中間人攻擊,所以雙向認(rèn)證解決的問題就是防止中間人攻擊。
中間人攻擊(Man-in-the-MiddleAttack)簡稱(MITM),是一種“間接”的入侵攻擊,這種攻擊模式是通過各種技術(shù)手段將受入侵者控制的一臺計算機虛擬放置在網(wǎng)絡(luò)連接中的兩臺通信計算機之間,這臺計算機就稱為“中間人”。若沒有開啟雙向認(rèn)證,中間人可以攔截客戶端發(fā)送的請求,然后篡改信息再發(fā)送到服務(wù)端;中間人也可以攔截服務(wù)端返回的信息,再發(fā)送到客戶端。所以使用Https的單向認(rèn)證或雙向認(rèn)證能夠有效防止中間人攻擊。
注:無論Ca證書還是自簽證書都需要雙向認(rèn)證。
雙向認(rèn)證原理
1、服務(wù)端認(rèn)證客戶端原理
客戶端有自己的bks證書auth_client.bks,并將導(dǎo)出的auth_client_pub.cer證書導(dǎo)入到服務(wù)端證書auth_server.keystore中,這樣服務(wù)端就將客戶端證書添加到信任列表中,從而能夠讓帶有該auth_client_pub.cer證書信息的客戶端訪問服務(wù)。
2、客戶端認(rèn)證服務(wù)端原理
服務(wù)端有自己的證書(ca頒發(fā)的或者是自己創(chuàng)建的)auth_server.keystore,并導(dǎo)出auth_server_pub.cer證書,將該證書導(dǎo)入到客戶端證書
auth_truststore.jks中,注意:這里不是導(dǎo)入到auth_client.jks中,而是導(dǎo)入生成另一個證書auth_truststore.jks,最后再將jks證書轉(zhuǎn)化成bks證書。
實現(xiàn)過程
一、服務(wù)端證書
創(chuàng)建服務(wù)端證書
keytool -genkeypair -alias auth_server -keyalg RSA -validity 36500 -keypass auth_server -storepass auth_server -keystore /Users/renzhongrui/android/certs/auth_server.keystore
導(dǎo)出服務(wù)端證書公鑰
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
二、客戶端證書
創(chuàng)建客戶端證書(andoird不能用keystore格式的密鑰庫,所以先生成jks格式,再用Portecle工具轉(zhuǎn)成bks格式)
keytool -genkeypair -alias auth_client -keyalg RSA -validity 36500 -keypass auth_client -storepass auth_client -keystore /Users/renzhongrui/android/certs/auth_client.jks
導(dǎo)出客戶端證書公鑰
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
三、證書交換
將客戶端證書導(dǎo)入服務(wù)端keystore中,再將服務(wù)端證書導(dǎo)入客戶端auth_truststore中, 一個keystore可以導(dǎo)入多個證書,生成證書列表。
將客戶端公鑰導(dǎo)入到服務(wù)端keystore證書中,使得服務(wù)端能夠信任客戶端。
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,即將服務(wù)端公鑰導(dǎo)入到客戶端jks證書中,使得客戶端能夠信任服務(wù)端。
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
四、證書轉(zhuǎn)換
下載portecle.jar(https://sourceforge.net/projects/portecle/),解壓后運行jar包:
java -jar portecle.jar
1、點擊File菜單選擇Open Keystore File,選擇創(chuàng)建好的auth_client.jks或auth_truststore.jks證書,輸入密碼。
2、選中導(dǎo)入的證書,點擊Tools菜單,選擇Change Keystore Type,再選擇BKS類型,再次輸入密碼,確認(rèn)之后,會顯示成功。
3、最后點擊File菜單,選擇Save Keystore File As,將證書保存的指定路徑。
五、配置服務(wù)
證書準(zhǔn)備好之后,就可以進行集成測試了,服務(wù)使用Spring Boot創(chuàng)建或者使用Nginx代理。
使用Spring Boot服務(wù)
1、添加配置
server:
port: 443
server:
tomcat:
uri-encoding: UTF-8
開啟https,配置跟證書對應(yīng)
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
#是否需要進行認(rèn)證
client-auth: need
protocol: TLS # 默認(rèn)
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服務(wù)配置
Nginx配置與Spring Boot服務(wù)配置略有不同。
server {
listen 443;
server_name 192.168.200.101; # 代理服務(wù)IP
ssl on; # 開啟Https
ssl_certificate /usr/local/nginx/conf/https/auth_server.cer; # auth_server.keystore導(dǎo)出的cer證書
ssl_certificate_key /usr/local/nginx/conf/https/auth_server.key; # auth_server.keystore導(dǎo)出的私鑰
ssl_client_certificate /usr/local/nginx/conf/https/auth_client.cer; # auth_client.keystore導(dǎo)出的cer
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_verify_client optional; # 配置校驗客戶端策略,設(shè)置成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來進行網(wǎng)絡(luò)訪問,所以需要配置OkHttp來進行證書認(rèn)證。
1、將上面創(chuàng)建的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類的實現(xiàn):
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 {
// 服務(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 (Exception e) {
e.printStackTrace();
}
return sslSocketFactory;
}
}
還有一個UnSafeHostnameVerifier類
private class UnSafeHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
此時再進行網(wǎng)絡(luò)請求,就能夠訪問到帶有雙向認(rèn)證的服務(wù)端接口了。當(dāng)然一般網(wǎng)上的博客到這就結(jié)束了,但是這樣真的就完事了嗎,其實真正的設(shè)計才剛開始,如果只是了解原理,讀到這里就可以了,下面才是真實應(yīng)用場景。
真實場景實現(xiàn)
原理還是那個原理,就看怎么合理的使用了。在真實開發(fā)環(huán)境中,需要解決幾個問題:
auth_client.bks和auth_truststore.bks是需要動態(tài)下發(fā)的
不是所有的接口都需要進行雙向認(rèn)證
動態(tài)下發(fā)auth_client.bks和auth_truststore.bks
1、auth_client.bks和auth_truststore.bks的制作需要在本地工具完成,然后通過管理端上傳到服務(wù)器,并且改變證書的版本號;
2、客戶端需要訪問證書版本,來判斷是否需要更新證書,如果需要更新則下載證書。
這里會引出兩個問題:
1、請求版本號的接口和下載證書的接口不能進行雙向認(rèn)證,否則無法下發(fā)證書。
2、不進行雙向認(rèn)證的接口是不安全的,所以,請求版本號的接口的返回值是需要加密的;
針對第一個問題處理方式:
服務(wù)端需要配置白名單,將請求版本號的接口和下載證書的接口過濾掉;
客戶端OkHttp首次初始化不能進行雙向認(rèn)證,等下載完證書之后,需要再次進行OkHttp初始化;
針對第二個問題處理方式:
需要本地工具創(chuàng)建RSA公私鑰對,用于請求版本號接口的加解密;
服務(wù)端使用私鑰對報文加密,客戶端保存公鑰,并使用公鑰對報文解密。
客戶端使用公鑰解密后的報文格式:
{
"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 表示不開啟認(rèn)證,1 表示開啟單向認(rèn)證,2 表示開啟雙向認(rèn)證
clientBksPath:auth_client.bks下載路徑
trustBksPath:auth_truststore.bks下載路徑
authKey:auth_client.bks證書密碼
客戶端每次啟動都要獲取服務(wù)端證書版本,并將證書信息存儲到本地文件或者數(shù)據(jù)庫中,通過對比服務(wù)端證書版本和數(shù)據(jù)庫中版本來判斷是否需要證書更新。
注:這樣設(shè)計的好處是當(dāng)證書過期時,能夠動態(tài)下發(fā)證書,但會引出一個問題,客戶端要安全的存儲公鑰信息,一般做法是將公鑰存儲到so文件里,再配合應(yīng)用加固手段進行保護,不過這個就不是通信安全的問題了,而是apk安全的問題。
其他證書操作
1、查看keystore證書公鑰
keytool -list -rfc --keystore release.keystore | openssl x509 -inform pem -pubkey
2、查看keystore證書私鑰
先轉(zhuǎn)成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