如何讓你的傳輸更安全--基於SSL協議的通訊模式
之前發表在另一個平臺的文章http://www.jointforce.com/jfperiodical/article/1218,現在發表到自己的部落格上。
對於SSL/TLS協議,如果要每個開發者都自己去實現顯然會帶來不必要的麻煩,正是為了解決這個問題Java為廣大開發者提供了Java安全套接字擴充套件——JSSE,它包含了實現Internet安全通訊的一系列包的集合,是SSL和TLS的純Java實現,同時它是一個開放的標準,每個公司都可以自己實現JSSE,通過它可以透明地提供資料加密、伺服器認證、資訊完整性等功能,就像使用普通的套接字一樣使用安全套接字,大大減輕了開發者的負擔,使開發者可以很輕鬆將SSL協議整合到程式中,並且JSSE能將安全隱患降到了最低點。
在利用SSL/TLS進行安全通訊時,客戶端跟伺服器端都必須要支援SSL/TLS協議,不然將無法進行通訊。而且客戶端和伺服器端都可能要設定用於證實自己身份的安全證書,並且還要設定信任對方的哪些安全證書。
關於身份認證方面有個名詞叫客戶端模式,一般情況客戶端要對伺服器端的身份進行驗證,但是無需向伺服器證實自己的身份,這樣不用向對方證實自己身份的通訊端我們就說它處於客戶模式,否則成它處於伺服器模式。SSLSocket的setUseClientMode(Boolean mode)方法可以設定客戶端模式或伺服器模式。
下面看具體實現的過程
① 解決證書問題。
一般而言作為伺服器端必須要有證書以證明這個伺服器的身份,並且證書應該描述此伺服器所有者的一些基本資訊,例如公司名稱、聯絡人名等。證書由所有人以密碼形式簽名,基本不可偽造,證書獲取的途徑有兩個:一是從權威機構購買證書,權威機構擔保它發出的證書的真實性,而且這個權威機構被大家所信任,進而你可以相信這個證書的有效性;另外一個是自己用JDK提供的工具keytool建立一個自我簽名的證書,這種情況下一般是我只想要保證資料的安全性與完整性,避免資料在傳送的過程中被竊聽或篡改,此時身份的認證已不重要,重點已經在端與端傳輸的祕密性上,證書的作用只體現在加解密簽名。
SSL協議通訊涉及金鑰儲存的檔案格式比較多,很容易搞混,例如xxx.cer、xxx.pfx、xxx.jks、xxx.keystore、xxx.truststore等格式檔案。如圖3-1-7-3,搞清楚他們有助於理解後面的程式,.cer格式檔案俗稱證書,但這個證書中沒有私鑰,只包含了公鑰;.pfx格式檔案也稱為證書,它一般供瀏覽器使用,而且它不僅包含了公鑰,還包含了私鑰,當然這個私鑰是加密的,不輸入密碼是解不了密的;.jks格式檔案表示java金鑰儲存器(java key store),它可以同時容納N個公鑰跟私鑰,是一個金鑰庫;.keystore格式檔案其實跟.jks基本是一樣的,只是不同公司叫法不太一樣,預設生成的證書儲存庫格式;.truststore格式檔案表示信任證書儲存庫,它僅僅包含了通訊對方的公鑰,當然你可以直接把通訊對方的jks作為信任庫(就算如此你也只能知道通訊對方的公鑰,要知道金鑰都是加密的,你無從獲取,只要演算法不被破解)。有些時候我們需要把pfx或cert轉化為jks以便於用java進行ssl通訊,例如一個銀行只提供了pfx證書,而我們想用java進行ssl通訊時就要將pfx轉化為jks格式。
圖3-1-7-3 金鑰儲存檔案格式
按照理論上,我們一共需要準備四個檔案,兩個keystore檔案和兩個truststore檔案,通訊雙方分別擁有一個keystore和一個truststore,keystore用於存放自己的金鑰和公鑰,truststore用於存放所有需要信任方的公鑰。這裡為了方便直接使用jks即keystore替代truststore(免去證書導來導去),因為對方的keystore包含了自己需要的信任公鑰。
下面使用jdk自帶的工具分別生成伺服器端證書,通過如下命令並輸入姓名、組織單位名稱、組織名稱、城市、省份、國家資訊即可生成證書密碼為tomcat的證書,此證書存放在密碼也為tomcat的tomcat.jks證書儲存庫中。如果你繼續建立證書將繼續往tomcat.jks證書儲存庫中新增證書。如果你僅僅輸入keytool -genkey -alias tomcat -keyalg RSA -keypass tomcat -storepass tomcat,不指定證書儲存庫的檔名及路徑,則工具會在使用者的home directory目錄下生產一個“.keystore”檔案作為證書儲存庫。
類似的,客戶端證書也用此方式進行生成。如下
② 服務端TomcatSSLServer.java
public class TomcatSSLServer {
private static final String SSL_TYPE = "SSL";
private static final String KS_TYPE = "JKS";
private static final String X509 = "SunX509";
private final static int PORT = 443;
private static TomcatSSLServer sslServer;
private SSLServerSocket svrSocket;
public static TomcatSSLServer getInstance() throws Exception {
if (sslServer == null) {
sslServer = new TomcatSSLServer();
}
return sslServer;
}
private TomcatSSLServer() throws Exception{
SSLContext sslContext = createSSLContext();
SSLServerSocketFactory serverFactory = sslContext.getServerSocketFactory();
svrSocket =(SSLServerSocket) serverFactory.createServerSocket(PORT);
svrSocket.setNeedClientAuth(true);
String[] supported = svrSocket.getEnabledCipherSuites();
svrSocket.setEnabledCipherSuites(supported);
}
private SSLContext createSSLContext() throws Exception{
KeyManagerFactory kmf = KeyManagerFactory.getInstance(X509);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(X509);
String serverKeyStoreFile = "c:\\tomcat.jks";
String svrPassphrase = "tomcat";
char[] svrPassword = svrPassphrase.toCharArray();
KeyStore serverKeyStore = KeyStore.getInstance(KS_TYPE);
serverKeyStore.load(new FileInputStream(serverKeyStoreFile), svrPassword);
kmf.init(serverKeyStore, svrPassword);
String clientKeyStoreFile = "c:\\client.jks";
String cntPassphrase = "client";
char[] cntPassword = cntPassphrase.toCharArray();
KeyStore clientKeyStore = KeyStore.getInstance(KS_TYPE);
clientKeyStore.load(new FileInputStream(clientKeyStoreFile),cntPassword);
tmf.init(clientKeyStore);
SSLContext sslContext = SSLContext.getInstance(SSL_TYPE);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return sslContext;
}
public void startService() {
SSLSocket cntSocket = null;
BufferedReader ioReader = null;
PrintWriter ioWriter = null;
String tmpMsg = null;
while( true ) {
try {
cntSocket =(SSLSocket) svrSocket.accept();
ioReader = new BufferedReader(new InputStreamReader(cntSocket.getInputStream()));
ioWriter = new PrintWriter(cntSocket.getOutputStream());
while ( (tmpMsg = ioReader.readLine()) != null) {
System.out.println("客戶端通過SSL協議傳送資訊:"+tmpMsg);
tmpMsg="歡迎通過SSL協議連線";
ioWriter.println(tmpMsg);
ioWriter.flush();
}
} catch(IOException e) {
e.printStackTrace();
} finally {
try {
if(cntSocket != null) cntSocket.close();
} catch(Exception ex) {ex.printStackTrace();}
}
}
}
public static void main(String[] args) throws Exception {
TomcatSSLServer.getInstance().startService();
}
}
基本順序是先得到一個SSLContext例項,再對SSLContext例項進行初始化,金鑰管理器及信任管理器作為引數傳入,證書管理器及信任管理器按照指定的金鑰儲存器路徑和密碼進行載入。接著設定支援的加密套件,最後讓SSLServerSocket開始監聽客戶端傳送過來的訊息。
③ 客戶端TomcatSSLClient.java
public class TomcatSSLClient {
private static final String SSL_TYPE = "SSL";
private static final String X509 = "SunX509";
private static final String KS_TYPE = "JKS";
private SSLSocket sslSocket;
public TomcatSSLClient(String targetHost,int port) throws Exception {
SSLContext sslContext = createSSLContext();
SSLSocketFactory sslcntFactory =(SSLSocketFactory) sslContext.getSocketFactory();
sslSocket = (SSLSocket) sslcntFactory.createSocket(targetHost, port);
String[] supported = sslSocket.getSupportedCipherSuites();
sslSocket.setEnabledCipherSuites(supported);
}
private SSLContext createSSLContext() throws Exception{
KeyManagerFactory kmf = KeyManagerFactory.getInstance(X509);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(X509);
String clientKeyStoreFile = "c:\\client.jks";
String cntPassphrase = "client";
char[] cntPassword = cntPassphrase.toCharArray();
KeyStore clientKeyStore = KeyStore.getInstance(KS_TYPE);
clientKeyStore.load(new FileInputStream(clientKeyStoreFile),cntPassword);
String serverKeyStoreFile = "c:\\tomcat.jks";
String svrPassphrase = "tomcat";
char[] svrPassword = svrPassphrase.toCharArray();
KeyStore serverKeyStore = KeyStore.getInstance(KS_TYPE);
serverKeyStore.load(new FileInputStream(serverKeyStoreFile), svrPassword);
kmf.init(clientKeyStore, cntPassword);
tmf.init(serverKeyStore);
SSLContext sslContext = SSLContext.getInstance(SSL_TYPE);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return sslContext;
}
public String sayToSvr(String sayMsg) throws IOException{
BufferedReader ioReader = new BufferedReader(new InputStreamReader(
sslSocket.getInputStream()));
PrintWriter ioWriter = new PrintWriter(sslSocket.getOutputStream());
ioWriter.println(sayMsg);
ioWriter.flush();
return ioReader.readLine();
}
public static void main(String[] args) throws Exception {
TomcatSSLClient sslSocket = new TomcatSSLClient("127.0.0.1",443);
BufferedReader ioReader = new BufferedReader(new InputStreamReader(System.in));
String sayMsg = "";
String svrRespMsg= "";
while( (sayMsg = ioReader.readLine())!= null ) {
svrRespMsg = sslSocket.sayToSvr(sayMsg);
if(svrRespMsg != null && !svrRespMsg.trim().equals("")) {
System.out.println("伺服器通過SSL協議響應:"+svrRespMsg);
}
}
}
}
客戶端的前面操作基本跟伺服器端的一樣,先建立一個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實現BIO模式的SSL協議通訊的例子已完成。
========廣告時間========鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以到 https://item.jd.com/12185360.html 進行預定。感謝各位朋友。[為什麼寫《Tomcat核心設計剖析》](http://blog.csdn.net/wangyangzhizhou/article/details/74080321)=========================歡迎關注:![這裡寫圖片描述](https://img-blog.csdn.net/20170509102539658?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2FuZ3lhbmd6aGl6aG91/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
========廣告時間========鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以到 https://item.jd.com/12185360.html 進行預定。感謝各位朋友。[為什麼寫《Tomcat核心設計剖析》](http://blog.csdn.net/wangyangzhizhou/article/details/74080321)=========================歡迎關注:![這裡寫圖片描述](https://img-blog.csdn.net/20170509102539658?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2FuZ3lhbmd6aGl6aG91/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)