雙向驗證即: 客戶端與服務端在進行數據傳輸的時候雙方都需要驗證對方的身份。
在建立 Https 連接的工程中,握手的流程比單向認證多了幾步。
-
單向認證的過程
客戶端從服務器端下載服務器公鑰證書進行驗證,然后才建立安全通信通道
-
雙向認證的過程
客戶端出了需要從服務器端下葬服務器的公鑰證書進行驗證外,還需要把客戶端的公鑰證書上傳到服務器給服務器端進行驗證,等雙方認證都認證通過之后,才會開始進行安全通信通道
1.1 單向認證流程
單向認證流程中,服務器端保存着公鑰證書和私鑰兩個文件,整個握手過程如下:
-
客戶端發青建立 HTTPS 連接請求,將 SSL 版本協議的信息發送給服務器端
-
服務器端將本機的公鑰證書 ( server.crt ) 發送給客戶端
-
客戶端讀取公鑰證書 ( server.crt ) , 獲取服務器端的公鑰
-
客戶端生成一個隨機數(密鑰R),用剛才得到的服務器公鑰去加密這個隨機數形成密文,發送給服務端;
-
服務端用自己的私鑰(server.key)去解密這個密文,得到了密鑰R
-
服務端和客戶端在后續通訊過程中就使用這個密鑰R進行通信了。
1.2 雙向認證流程
-
客戶端發起建立HTTPS連接請求,將SSL協議版本的信息發送給服務端;
-
服務器端將本機的公鑰證書(server.crt)發送給客戶端;
-
客戶端讀取公鑰證書(server.crt),取出了服務端公鑰;
-
客戶端將客戶端公鑰證書(client.crt)發送給服務器端;
-
服務器端使用根證書(root.crt)解密客戶端公鑰證書,拿到客戶端公鑰;
-
客戶端發送自己支持的加密方案給服務器端;
-
服務器端根據自己和客戶端的能力,選擇一個雙方都能接受的加密方案,使用客戶端的公鑰加密后發送給客戶端;
-
客戶端使用自己的私鑰解密加密方案,生成一個隨機數R,使用服務器公鑰加密后傳給服務器端;
-
服務端用自己的私鑰去解密這個密文,得到了密鑰R
-
服務端和客戶端在后續通訊過程中就使用這個密鑰R進行通信了。
2. cert 、 key 和 pem 文件區別
推薦閱讀:https://blog.csdn.net/justinjing0612/article/details/7770301
我們當前獲取有3個文件,分別是
-
ca-server.cert.pem
我們請求到的服務器給頒布的公鑰
-
vendor.my.cert.pem
我們本地發送客戶端的公鑰 ( 需要發送到服務器端)
-
vendor.my.key.pem
我們本地客戶端的密鑰
我們可以轉換為我們自己想要的格式
所有的 key 密碼推薦使用 changeit
默認值
先將 pem 轉換為jks ,因為沒有密鑰,所以轉換為 JKS 作為 trustStore 使用
keytool -import -noprompt -file ca-server.cert.pem -keystore ca-server.jks -storepass changeit
而且我們仍要攜帶客戶端的 vendor.my.cert.pem 推送給服務器,而 Java 客戶端無法處理 pem文件,需要將其轉換為 .der 文件或者 p12 文件。
可以通過如下指令
openssl pkcs12 -export -clcerts -in vendor.yundun.cert.pem -inkey vendor.yundun.key.pem -out vendor.yundun.p12
也可以將 pkcs12 轉換為 JKS 不推薦
keytool -importkeystore -srckeystore vendor.my.p12 -srcstoretype PKCS12 -deststoretype JKS -destkeystore vendor.my.jks
Java 官方也不建議將 PKCS12 轉換為 JKS
正在將密鑰庫 vendor.my.p12 導入到 vendor.my.jks... 輸入目標密鑰庫口令: 輸入源密鑰庫口令: 已成功導入別名 1 的條目。 已完成導入命令: 1 個條目成功導入, 0 個條目失敗或取消
Warning: JKS 密鑰庫使用專用格式。建議使用 "keytool -importkeystore -srckeystore vendor.my.jks -destkeystore vendor.my.jks -deststoretype pkcs12" 遷移到行業標准格式 PKCS12。
3 基於 JAVA 實現 HTTPS 雙向綁定請求
3.1 基於內部代碼實現
3.1.1 證書在 Jar 外部
使用 Java 代碼實現 Https 雙向綁定請求
private final static String CLIENT_PFX_PATH = "/{your_path}/vendor.my.p12"; //客戶端證書路徑 private final static String CLIENT_PFX_PWD = "password"; //客戶端證書密碼 private final static String SERVER_PFX_PATH = "/{your_path}/ca-server.jks"; //服務器端證書路徑 private final static String SERVER_PFX_PWD = "password"; // 服務器端證書密碼 public static void sslRequestGet(String url) throws Exception { CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(getSSLFactory()).build(); try { HttpGet httpget = new HttpGet(url); CloseableHttpResponse response = httpclient.execute(httpget); try { HttpEntity entity = response.getEntity(); String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");//返回結果 EntityUtils.consume(entity); System.out.println(jsonStr); } finally { response.close(); } } finally { httpclient.close(); } } public static SSLConnectionSocketFactory getSSLFactory() throws Exception{ // 此時需要查看對應的文件格式,如果公鑰密鑰都存在,建議使用 PKCS12 標准格式 // 如果只存在公鑰,建議使用 JAVA 支持的 JKS 格式 ( PKCS12 必須要求公密鑰) // 轉換 command 在 #2 KeyStore serverKeyStore = KeyStore.getInstance("jks"); KeyStore clientKeyStore = KeyStore.getInstance("PKCS12"); try (InputStream clientStream = new FileInputStream(CLIENT_PFX_PATH); InputStream serverStream = new FileInputStream(SERVER_PFX_PATH)){ clientKeyStore.load(clientStream, CLIENT_PFX_PWD.toCharArray()); serverKeyStore.load(serverStream, SERVER_PFX_PWD.toCharArray()); } KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(clientKeyStore, CLIENT_PFX_PWD.toCharArray()); KeyManager[] keyManagers = kmf.getKeyManagers(); TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(serverKeyStore); TrustManager[] trustManagers = tmf.getTrustManagers(); SSLContext sslcontext = SSLContext.getInstance("TLS"); sslcontext.init(keyManagers, trustManagers, new java.security.SecureRandom()); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext , new String[]{"TLSv1"} // supportedProtocols ,這里可以按需要設置 , null // supportedCipherSuites , SSLConnectionSocketFactory.getDefaultHostnameVerifier()); return sslsf; }
基於純內部代碼實現, 其中各個公密鑰的 Path 推薦放置在打包好的 Jar 文件外部
3.1.2 證書在 Jar 內部
這樣可以將證書打包在 Jar 內部,更好在 K8S 或者 CICD 中部署繼承。
java 代碼
private final static String CLIENT_PFX_PATH = "/{your_path}/vendor.my.p12"; //客戶端證書路徑 private final static String CLIENT_PFX_PWD = "password"; //客戶端證書密碼 private final static String SERVER_PFX_PATH = "/{your_path}/ca-server.jks"; //服務器端證書路徑 private final static String SERVER_PFX_PWD = "password"; // 服務器端證書密碼 public static void sslRequestGet(String url) throws Exception { CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(getSSLFactory()).build(); try { HttpGet httpget = new HttpGet(url); CloseableHttpResponse response = httpclient.execute(httpget); try { HttpEntity entity = response.getEntity(); String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");//返回結果 EntityUtils.consume(entity); System.out.println(jsonStr); } finally { response.close(); } } finally { httpclient.close(); } } public static SSLConnectionSocketFactory getSSLFactory() throws Exception{ // 此時需要查看對應的文件格式,如果公鑰密鑰都存在,建議使用 PKCS12 標准格式 // 如果只存在公鑰,建議使用 JAVA 支持的 JKS 格式 ( PKCS12 必須要求公密鑰) // 轉換 command 在 #2 KeyStore serverKeyStore = KeyStore.getInstance(SERVER_PFX_TYPE); KeyStore clientKeyStore = KeyStore.getInstance(CLIENT_PFX_TYPE); try (InputStream clientStream = getClass().getClassLoader().getResourceAsStream(CLIENT_PFX_PATH); InputStream serverStream = getClass().getClassLoader().getResourceAsStream(SERVER_PFX_PATH)) { clientKeyStore.load(clientStream, CLIENT_PFX_PWD.toCharArray()); serverKeyStore.load(serverStream, SERVER_PFX_PWD.toCharArray()); } KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(clientKeyStore, CLIENT_PFX_PWD.toCharArray()); KeyManager[] keyManagers = kmf.getKeyManagers(); TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(serverKeyStore); TrustManager[] trustManagers = tmf.getTrustManagers(); SSLContext sslcontext = SSLContext.getInstance("TLS"); sslcontext.init(keyManagers, trustManagers, new java.security.SecureRandom()); · SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext , new String[]{"TLSv1"} // supportedProtocols ,這里可以按需要設置 , null // supportedCipherSuites , SSLConnectionSocketFactory.getDefaultHostnameVerifier()); return sslsf; }
需要在 resources/httpsKeys
文件夾下存放相對應的文件
3.2 使用命令行指定文件
Java代碼為:
public static void request(String url) { CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(SSLConnectionSocketFactory.getSystemSocketFactory()).build(); HttpGet get = new HttpGet(url); System.out.println("trying to execute request"); try (CloseableHttpResponse response = httpClient.execute(get)) { HttpEntity entity = response.getEntity(); System.out.println(response.getStatusLine().getStatusCode()); String s = EntityUtils.toString(entity); System.out.println(s); System.out.println("finished request"); } catch (IOException e) { e.printStackTrace(); } }
java -Djavax.net.ssl.keyStore=./vendor.my.p12 -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.keyStoreType=PKCS12 -Djavax.net.ssl.trustStore=./ca-server.jks -Djavax.net.ssl.trustStorePassword=changeit -jar https_test.jar
其中只需要指定好各個文件的位置、密碼以及類型,即可通過 SSLConnectionSocketFactory.getSystemSocketFactory()
實現
3.3 在 JVM LIB 中添加密鑰
我們可以直接在 Java 中導入 ca-chain.cert.pem ,服務器的公鑰,通過如下命令
不推薦
sudo keytool -import -alias log-collect-jh.bs58i.baishancdnx.com -keystore cmycerts -file /{your_path}/ca-server.cert.pem
使用方法同 #3.2