Java中使用JSSE實現SSL/TLS安全協議
一、簡介
SSL/TLS協議是安全的通訊模式,而對於這些底層協議,如果要每個開發者都自己去實現顯然會帶來不必要的麻煩,正是為了解決這個問題Java為廣大開發者提供了Java安全套接字擴充套件——JSSE,它包含了實現Internet安全通訊的一系列包的集合,是SSL和TLS的純Java實現,同時它是一個開放的標準,每個公司都可以自己實現JSSE,通過它可以透明地提供資料加密、伺服器認證、資訊完整性等功能,就像使用普通的套接字一樣使用安全套接字,大大減輕了開發者的負擔,使開發者可以很輕鬆將SSL協議整合到程式中,並且JSSE能將安全隱患降到了最低點。
在用JSSE實現SSL通訊過程中主要會遇到以下類和介面,由於過程中涉及到加解密、金鑰生成等運算的框架和實現,所以也會間接用到JCE包的一些類。如圖3-1-7-2為JSSE介面的主要類圖:
① 通訊核心類——SSLSocket和SSLServerSocket。它們對應的就是Socket與ServerSocket,只是表示實現了SSL協議的Socket,ServerSocket,同時它們也是Socket與ServerSocket的子類。SSLSocket負責的事情包括【設定加密套件、管理SSL會話、處理握手結束時間、設定客戶端模式或伺服器模式】。 ② 客戶端與伺服器端Socket工廠——SSLSocketFactory和SSLServerSocketFactory。在設計模式中工廠模式是專門用於生產出需要的例項,這裡也是把SSLSocket、SSLServerSocket物件建立的工作交給這兩個工廠類。 ③ SSL會話——SSLSession。安全通訊握手過程需要一個會話,為了提高通訊的效率,SSL協議允許多個SSLSocket共享同一個SSL會話,在同一個會話中,只有第一個開啟的SSLSocket需要進行SSL握手,負責生成金鑰及交換金鑰,其餘SSLSocket都共享金鑰資訊。 ④ SSL上下文——SSLContext。它是對整個SSL/TLS協議的封裝,表示了安全套接字協議的實現。主要負責設定安全通訊過程中的各種資訊,例如跟證書相關的資訊。並且負責構建SSLSocketFactory、SSLServerSocketFactory和SSLEngine等工廠類。 ⑤ SSL非阻塞引擎——SSLEngine。假如你要進行NIO通訊,那麼將使用這個類,它讓通過過程支援非阻塞的安全通訊。 ⑥ 金鑰管理器——KeyManager。此介面負責選擇用於證實自己身份的安全證書
二、證書簡介
看一個用JSSE簡單實現SSL通訊的例子。 ① 解決證書問題。 一般而言作為伺服器端必須要有證書以證明這個伺服器的身份,並且證書應該描述此伺服器所有者的一些基本資訊,例如公司名稱、聯絡人名等。證書由所有人以密碼形式簽名,基本不可偽造,證書獲取的途徑有兩個:一是從權威機構購買證書,權威機構擔保它發出的證書的真實性,而且這個權威機構被大家所信任,進而你可以相信這個證書的有效性;另外一個是自己用JDK提供的工具keytool建立一個自我簽名的證書,這種情況下一般是我只想要保證資料的安全性與完整性,避免資料在傳送的過程中被竊聽或篡改,此時身份的認證已不重要,重點已經在端與端傳輸的祕密性上,證書的作用只體現在加解密簽名。 另外,關於證書的一些概念在這裡陳述,一個證書是一個實體的數字簽名,這個實體可以是一個人、一個組織、一個程式、一個公司、一個銀行,同時證書還包含這個實體的公共鑰匙,此公共鑰匙是這個實體的數字關聯,讓所有想同這個實體發生信任關係的其他實體用來檢驗簽名。而這個實體的數字簽名是實體資訊用實體的私鑰加密後的資料,這條資料可以用這個實體的公共鑰匙解密,進而鑑別實體的身份。這裡用到的核心演算法是非對稱加密演算法。
SSL協議通訊涉及金鑰儲存的檔案格式比較多,很容易搞混,例如xxx.cer、xxx.pfx、xxx.jks、xxx.keystore、xxx.truststore等格式檔案。如圖3-1-7-3,搞清楚他們有助於理解後面的程式,.cer格式檔案俗稱證書,但這個證書中沒有私鑰,只包含了公鑰;.pfx格式檔案也稱為證書,它一般供瀏覽器使用,而且它不僅包含了公鑰,還包含了私鑰,當然這個私鑰是加密的,不輸入密碼是解不了密的;.jks格式檔案表示java金鑰儲存器(javakey store),它可以同時容納N個公鑰跟私鑰,是一個金鑰庫;.keystore格式檔案其實跟.jks基本是一樣的,只是不同公司叫法不太一樣,預設生成的證書儲存庫格式;.truststore格式檔案表示信任證書儲存庫,它僅僅包含了通訊對方的公鑰,當然你可以直接把通訊對方的jks作為信任庫(就算如此你也只能知道通訊對方的公鑰,要知道金鑰都是加密的,你無從獲取,只要演算法不被破解)。有些時候我們需要把pfx或cert轉化為jks以便於用java進行ssl通訊,例如一個銀行只提供了pfx證書,而我們想用java進行ssl通訊時就要將pfx轉化為jks格式。
三、生成證書,
按照理論上,我們一共需要準備四個檔案,兩個keystore檔案和兩個truststore檔案,通訊雙方分別擁有一個keystore和一個truststore,keystore用於存放自己的金鑰和公鑰,truststore用於存放所有需要信任方的公鑰。這裡為了方便直接使用jks即keystore替代truststore(免去證書導來導去),因為對方的keystore包含了自己需要的信任公鑰。 下面使用jdk自帶的工具分別生成伺服器端證書,通過如下命令並輸入姓名、組織單位名稱、組織名稱、城市、省份、國家資訊即可生成證書密碼為myserver的證書,此證書存放在密碼也為123456的myserver.jks證書儲存庫中。如果你繼續建立證書將繼續往myserver.jks證書儲存庫中新增證書。
keytool用法:
1.生成
keytool -genkey -alias yushan(別名) -keypass yushan(別名密碼) -keyalg RSA(演算法) -keysize 1024(金鑰長度) -validity 365(有效期,天單位) -keystore e:\yushan.keystore(指定生成證書的位置和證書名稱) -storepass 123456(獲取keystore資訊的密碼);回車輸入相關資訊即可;
2.匯出(證書庫匯出到crt證書檔案)
keytool -export -alias yushan -keystore e:\yushan.keystore -file e:\yushan.crt(指定匯出的證書位置及證書名稱) -storepass 123456
3.匯入(從證書檔案匯入到keystore或jks檔案)
準備一個匯入的證書: keytool -genkey -alias shuany -keypass shuany -keyalg RSA -keysize 1024 -validity 365 -keystore e:\shuany.keystore -storepass 123456 -dname "CN=shuany, OU=xx, O=xx, L=xx, ST=xx, C=xx"; keytool -export -alias shuany -keystore e:\shuany.keystore -file e:\shuany.crt -storepass 123456 現在將shuany.crt 加入到yushan.keystore中:keytool -import -alias shuany(指定匯入證書的別名,如果不指定預設為mykey,別名唯一,否則匯入出錯) -file e:\shuany.crt -keystore e:\yushan.keystore -storepass 123456 keytool -list -v -keystore e:\keytool\yushan.keystore -storepass 123456
製作我的證書:
伺服器端:
客戶端:
生成檔案:
四、實現SSL
服務端簡單實現:
-
public static void main(String[] args) throws Exception {
-
//金鑰管理器
-
KeyStore serverKeyStore = KeyStore.getInstance("JKS");//證書庫格式
-
serverKeyStore.load(new FileInputStream("e:\\myserver.jks"), "123456".toCharArray());//載入金鑰庫
-
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");//證書格式
-
kmf.init(serverKeyStore, "123456".toCharArray());//載入金鑰儲存器
-
//信任管理器
-
KeyStore clientKeyStore = KeyStore.getInstance("JKS");
-
clientKeyStore.load(new FileInputStream("e:\\myclient.jks"), "123456".toCharArray());
-
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
-
tmf.init(clientKeyStore);
-
//SSL上下文設定
-
SSLContext sslContext = SSLContext.getInstance("SSL");
-
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
-
//SSLServerSocket
-
SSLServerSocketFactory serverFactory = sslContext.getServerSocketFactory();
-
SSLServerSocket svrSocket = (SSLServerSocket) serverFactory.createServerSocket(34567);
-
//svrSocket.setNeedClientAuth(true);//客戶端模式,服務端需要驗證客戶端身份
-
String[] supported = svrSocket.getEnabledCipherSuites();//加密套件
-
svrSocket.setEnabledCipherSuites(supported);
-
//接收訊息
-
System.out.println("埠已開啟,準備接受資訊");
-
SSLSocket cntSocket = (SSLSocket) svrSocket.accept();//開始接收
-
InputStream in=cntSocket.getInputStream();//輸入流
-
int a=in.read(new byte[102]);
-
//迴圈檢查是否有訊息到達
-
System.out.println("來自於客戶端:" + a);
-
}
基本順序是先得到一個SSLContext例項,再對SSLContext例項進行初始化,金鑰管理器及信任管理器作為引數傳入,證書管理器及信任管理器按照指定的金鑰儲存器路徑和密碼進行載入。接著設定支援的加密套件,最後讓SSLServerSocket開始監聽客戶端傳送過來的訊息。
客戶端實現:
-
public static void main(String[] args) throws Exception {
-
//金鑰管理器
-
KeyStore clientKeyStore = KeyStore.getInstance("JKS");
-
clientKeyStore.load(new FileInputStream("e:\\myclient.jks"), "123456".toCharArray());
-
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
-
kmf.init(clientKeyStore, "123456".toCharArray());
-
//信任管理器
-
KeyStore serverKeyStore = KeyStore.getInstance("JKS");
-
serverKeyStore.load(new FileInputStream("e:\\myserver.jks"), "123456".toCharArray());
-
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
-
tmf.init(serverKeyStore);
-
//SSL上下文
-
SSLContext sslContext = SSLContext.getInstance("SSL");
-
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
-
SSLSocketFactory sslcntFactory =(SSLSocketFactory) sslContext.getSocketFactory();
-
SSLSocket sslSocket= (SSLSocket) sslcntFactory.createSocket("127.0.0.1", 34567);
-
String[] supported = sslSocket.getSupportedCipherSuites();
-
sslSocket.setEnabledCipherSuites(supported);
-
//傳送
-
OutputStream out=sslSocket.getOutputStream();
-
out.write("hello".getBytes());
-
}
客戶端的前面操作基本跟伺服器端的一樣,先建立一個SSLContext例項,再用金鑰管理器及信任管理器對SSLContext進行初始化,當然這裡金鑰儲存的路徑是指向客戶端的client.jks。接著設定加密套件,最後使用SSLSocket進行通訊。 注意伺服器端有行程式碼svrSocket.setNeedClientAuth(true);它是非常重要的一個設定方法,用於設定是否驗證客戶端的身份。假如我們把它註釋掉或設定為false,此時客戶端將不再需要自己的金鑰管理器,即伺服器不需要通過client.jks對客戶端的身份進行驗證,把金鑰管理器直接設定為null也可以跟伺服器端進行通訊。
最後談談信任管理器,它的職責是覺得是否信任遠端的證書,那麼它憑藉什麼去判斷呢?如果不顯式設定信任儲存器的檔案路徑,將遵循如下規則:
①如果系統屬性javax.net.ssl.truststore指定了truststore檔案,那麼信任管理器將去jre路徑下的lib/security目錄尋找這個檔案作為信任儲存器;
②如果沒設定①中的系統屬性,則去尋找一個%java_home%/lib/security/jssecacerts檔案作為信任儲存器;
③如果jssecacerts不存在而cacerts存在,則cacerts作為信任儲存器。
至此,一個利用JSSE實現SSL協議通訊的例子已完成。