摘要:既然是雙向驗證,就需要雙方的密鑰,我們服務端稱爲 localhost ,而客戶端稱爲 client。服務端收到客戶端發送的消息後,使用自己的私鑰進行解密,獲取 對稱加密的密鑰 ,在接下來的會話中,服務器和客戶端將會使用該密碼進行對稱加密,保證通信過程中信息的安全。

1 簡介

不知不覺 Https 相關的文章已經寫了6篇了,本文將是這個專題的最後一篇,起碼近期是最後一篇。前面6篇講的全都是單向的 Https 驗證,本文將重點介紹一下雙向驗證。有興趣的同學可以瞭解一下之前的文章:

(0)Https專題

(1) Springboot整合https原來這麼簡單

(2) HTTPS之密鑰知識與密鑰工具Keytool和Keystore-Explorer

(3) Springboot以Tomcat爲容器實現http重定向到https的兩種方式

(4) Springboot以Jetty爲容器實現http重定向到https

(5) nginx開啓ssl並把http重定向到https的兩種方式

(6) Springboot-WebFlux實現http重定向到https

雙向驗證是比較難的,能掌握雙向驗證,單向驗證就沒什麼問題了。

2 單向驗證與雙向驗證

2.1 概念和作用

所謂 單向驗證One Way SSL ),就是隻有一方驗證另一方是否合法,通常是客戶端驗證服務端。比如我們打開 www.pkslow.com 這個網站,服務端不會驗證客戶端,只要你來我就歡迎。但客戶端不一樣,瀏覽器會驗證這個網站是不是安全的,一般是通過 CA 頒發的 SSL證書 來驗證。

雙向驗證Two Way SSL )則不同。不僅客戶端需要驗證服務端,服務端同樣戒備心很重,也需要驗證客戶端是否是合法。

大家是否會有疑問,這麼麻煩的 雙向驗證 有什麼用?我們平常用 單向驗證 不就已經足夠了嗎? 單向驗證 雖然安全,但不夠安全,使用 雙向驗證 可以只讓特定的客戶端訪問,安全性會高一點。

另一方面,如果服務端沒有做賬戶權限控制,但又想只限制特定的客戶端訪問, 雙向驗證 就非常有用,我只驗證我相信的客戶端,其它一概拒絕!這樣即使我的服務暴露在網上,也不怕別人訪問。既保證了安全,又省去了做權限控制的麻煩。

2.2 驗證合法性

驗證合法性通常是通過 Trust Store 。要求要把對方的 cert 裝在自己的 Trust Store 裏。

這就衍生出了一個之前沒有講過的概念: Trust Store 。密鑰文件可以存放私鑰和公鑰,具體一點來說是可以存放自己的私鑰、自己的公鑰和別人的公鑰(也可以存放別人的私鑰,但這不合理,私鑰必須私有)。一般地我們把自己的(私鑰和公鑰)存放在 Key Store 裏,而把別人的公鑰存放在 Trust Store 裏。

Java 程序爲例,我們建立 SSLContext 時,需要生成 KeyManagerTrustManager ,對應的參數爲 javax.net.ssl.keyStorejavax.net.ssl.trustStore 。而它們的作用正好體現了不同 Store 的作用:

  • TrustManager:決定對方來的 cert 是不是可信的;

  • KeyManager:決定自己發什麼 cert 給對方。

如果我們不指定 TrustStore ,默認是 $JAVA_HOME/jre/lib/security/cacerts 文件。我們可以通過命令查看這個文件都有什麼 certs

keytool -list -keystore cacerts

2.3 單向驗證的流程

單向驗證的流程如下圖所示:

建立連接的過程:

  1. 客戶端向服務端發送SSL協議版本號、加密算法種類、隨機數等信息;

  2. 服務端給客戶端返回SSL協議版本號、加密算法種類、隨機數等信息,同時也返回 服務器端的證書 ,即公鑰證書;

  3. 客戶端使用服務端返回的信息 驗證服務器端的合法性 ,包括:

    (1)證書是否過期

    (2)頒發服務器證書的CA是否可靠

    (3)返回的公鑰是否能正確解開返回證書中的數字簽名

    (4)服務器證書上的域名是否和服務器的實際域名相匹配

    驗證通過後,將繼續進行通信,否則,終止通信;

  4. 客戶端向服務端發送自己所能支持的 對稱加密方案 ,供服務器端進行選擇;

  5. 服務器端在客戶端提供的加密方案中選擇加密程度最高的 加密方式

  6. 服務器將選擇好的 加密方案 通過明文方式返回給客戶端;

  7. 客戶端接收到服務端返回的加密方式後,使用該加密方式生成產生隨機碼,用作通信過程中 對稱加密的密鑰 ,使用服務端返回的 公鑰進行加密 ,將加密後的隨機碼發送至服務器;

  8. 服務器收到客戶端返回的加密信息後,使用自己的 私鑰進行解密 ,獲取 對稱加密密鑰 。在接下來的會話中,服務器和客戶端將會使用該密碼進行對稱加密,保證通信過程中信息的安全。

2.4 雙向驗證的流程

雙向驗證的流程如下圖所示:

建立連接的過程:

  1. 客戶端向服務端發送SSL協議版本號、加密算法種類、隨機數等信息;

  2. 服務端給客戶端返回SSL協議版本號、加密算法種類、隨機數等信息,同時也返回 服務器端的證書 ,即公鑰證書;

  3. 客戶端使用服務端返回的信息 驗證服務器端的合法性 ,包括:

    (1)證書是否過期

    (2)頒發服務器證書的CA是否可靠

    (3)返回的公鑰是否能正確解開返回證書中的數字簽名

    (4)服務器證書上的域名是否和服務器的實際域名相匹配

    驗證通過後,將繼續進行通信,否則,終止通信;

  4. 服務端要求客戶端發送客戶端的證書,客戶端會將自己的 證書發送至服務端

  5. 驗證客戶端的證書,通過驗證後,會獲得 客戶端的公鑰

  6. 客戶端向服務端發送自己所能支持的 對稱加密方案 ,供服務器端進行選擇;

  7. 服務器端在客戶端提供的加密方案中選擇加密程度最高的 加密方式

  8. 將加密方案通過使用之前獲取到的 公鑰進行加密 ,返回給客戶端;

  9. 客戶端收到服務端返回的加密方案密文後,使用自己的 私鑰進行解密 ,獲取具體加密方式,而後,產生該加密方式的隨機碼,用作加密過程中的密鑰,使用之前從服務端證書中獲取到的公鑰進行加密後,發送給服務端;

  10. 服務端收到客戶端發送的消息後,使用自己的私鑰進行解密,獲取 對稱加密的密鑰 ,在接下來的會話中,服務器和客戶端將會使用該密碼進行對稱加密,保證通信過程中信息的安全。

3 Springboot整合雙向驗證

理論知識講完了,就來實戰一下, Springboot 是怎麼整合 雙向驗證 的。

3.1 生成密鑰文件

既然是雙向驗證,就需要雙方的密鑰,我們服務端稱爲 localhost ,而客戶端稱爲 client 。需要生成雙方的密鑰文件,並把對方的 cert 導入自己的密鑰文件裏。整個過程如下:

注意:密碼統一爲 changeit

# 生成服務端密鑰文件localhost.jks
keytool -genkey -alias localhost -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore localhost.jks -dname CN=localhost,OU=Test,O=pkslow,L=Guangzhou,C=CN -validity 731 -storepass changeit -keypass changeit

# 導出服務端的cert文件
keytool -export -alias localhost -file localhost.cer -keystore localhost.jks

# 生成客戶端的密鑰文件client.jks
keytool -genkey -alias client -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore client.jks -dname CN=client,OU=Test,O=pkslow,L=Guangzhou,C=CN -validity 731 -storepass changeit -keypass changeit

# 導出客戶端的cert文件
keytool -export -alias client -file client.cer -keystore client.jks

# 把客戶端的cert導入到服務端
keytool -import -alias client -file client.cer -keystore localhost.jks

# 把服務端的cert導入到客戶端
keytool -import -alias localhost -file localhost.cer -keystore client.jks

# 檢驗服務端是否具有自己的private key和客戶端的cert
keytool -list -keystore localhost.jks

成功執行完上述步驟後,會生成4個文件:

  • 服務端密鑰文件: localhost.jks

  • 服務端cert文件:localhost.cer

  • 客戶端密鑰文件:client.jks

  • 客戶端cert文件:client.cer

實際上 cert 文件可以不要了,因爲已經導入對方的 Trust Store 裏面去了。也就是在文件 localhost.jks 裏,包含了服務端的私鑰、公鑰還有客戶端的公鑰。 client.jks 同理。

3.2 配置Spirngboot

Springboot 的配置文件如下:

server.port=443

server.ssl.enabled=true
server.ssl.key-store-type=JKS
server.ssl.key-store=classpath:localhost.jks
server.ssl.key-store-password=changeit
server.ssl.key-alias=localhost

server.ssl.trust-store=classpath:localhost.jks
server.ssl.trust-store-password=changeit
server.ssl.trust-store-provider=SUN
server.ssl.trust-store-type=JKS
server.ssl.client-auth=need

需要分別配置 Key StoreTrust Store 的文件、密碼等信息,即使是同一個文件。

需要注意的是, server.ssl.client-auth 有三個可配置的值: nonewantneed 。雙向驗證應該配置爲 neednone 表示不驗證客戶端; want 表示會驗證,但不強制驗證,即驗證失敗也可以成功建立連接。

3.3 用Postman測試雙向驗證

完成密鑰文件準備和配置後,啓動 Springboot 便可以了。這裏用 Postman 訪問如下:

無法建立 https 連接,無法訪問。原因就是 Postman 作爲客戶端並沒有合法的cert。

爲了建立連接,應該要把客戶端的密鑰文件給 Postman 使用。因爲 JKS 是Java的密鑰文件格式,我們轉換成通用的 PKCS12 格式如下:

# 轉換JKS格式爲P12
keytool -importkeystore -srckeystore client.jks -destkeystore client.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass changeit -deststorepass changeit -srckeypass changeit -destkeypass changeit -srcalias client -destalias client -noprompt

把客戶端密鑰文件配置到 Postman 如下圖所示:

再重新訪問,成功了!

或者我們可以把密鑰文件拆成 private keycert ,命令如下:

# 導出客戶端的cert文件
openssl pkcs12 -nokeys -in client.p12 -out client.pem

# 導出客戶端的key文件
openssl pkcs12 -nocerts -nodes -in client.p12 -out client.key

把客戶端的密鑰文件配置到 Postman 上,如圖所示:

結果一樣是可以成功訪問:

3.4 用curl命令測試

沒有安裝 Postman 怎麼辦呢?還好,用強大的 curl 命令也是可以測試的。命令如下:

curl -k --cert client.pem --key client.key https://localhost/hello
# 下面命令可以查看建立SSL連接詳情
curl -v -k --cert client.pem --key client.key https://localhost/hello

如果覺得指定兩個文件太麻煩,可以只生成一個文件,命令如下:

openssl pkcs12 -nodes -in client.p12 -out client_all.pem

則連接命令變成了:

# 需要指定密碼
curl -k --cert client_all.pem:changeit https://localhost/hello

4 總結

這篇文章講解了單向驗證和雙向驗證的區別及流程,並用實例展示如何實現雙向驗證,相信跟着做一遍,基本都能理解了。

參考資料:

Trust Store vs Key Store - creating with keytool

Keystore vs. Truststore

An Overview of One-Way SSL and Two-Way SSL

驗證流程部分來自: Https單向認證和雙向認證

歡迎訪問 南瓜慢說 www.pkslow.com 獲取更多精彩文章!

歡迎關注微信公衆號< 南瓜慢說 >,將持續爲你更新...

多讀書,多分享;多寫作,多整理。

相關文章