當數據在網絡上傳播的時候,通過使用 SSL 對其進行加密和保護,JSSE 爲 Java 應用程序提供了安全的通信。在本篇有關該技術的高級研究中,Java 中間件開發人員 Ian Parkinson 深入研究了 JSSE API 較不爲人知的方面,爲您演示瞭如何圍繞 SSL 的一些限制進行編程。您將學習如何動態地選擇 KeyStore 和 TrustStore、放寬 JSSE 的密碼匹配要求,以及構建您自己定製的 KeyManager 實現。
JSSE(Java 安全套接字擴展,Java Secure Socket Extension)使 Java 應用程序能夠在因特網上使用 SSL 安全地進行通信。由於 developerWorks 已經提供了一篇涵蓋 JSSE 基本用法的教程(請參閱 參考資料),所以本文將集中闡述該技術的更高級用法。本文將演示如何使用 JSSE 接口定製 SSL 連接的屬性。
首先,我們將開發一個非常簡單的安全客戶機/服務器聊天應用程序。在我們構建該程序的客戶機端時,我將演示如何定製 KeyStore 和 TrustStore 文件,以便從客戶機的文件系統裝入它們。接着,我們將着重說明證書和標識。通過從 KeyStore 選擇不同的證書,可以將客戶機以不同的形式提供給不同的服務器。如果您的客戶機應用程序需要連接到多個對等方,或者甚至它需要冒充不同的用戶,這項高級的功能都特別有用。
由於本文着重講述更高級的主題,因此假定您已經具備了 JSSE 的使用經驗。要運行示例,需要一個帶有正確安裝和配置 JSSE 提供程序的 Java SDK。J2SE 1.4 SDK 提供了已安裝和配置的 JSSE。如果您正在使用 J2SE 1.2 或 1.3,則需要獲取一個 JSSE 實現並安裝它。請參閱 參考資料下載 JSSE 擴展。
JSSE API 只是 J2SE 1.4 的一項標準,並且早期的 JSSE 實現之間存在略有不同的變體。本文的示例基於 1.4 API。必要的時候,我會強調使示例與 J2SE 1.2 和 1.3 的 Sun JSSE 實現協同工作所必需的更改。
在我們深入研究 JSSE 之前,先讓我們熟悉一下將要使用的客戶機/服務器應用程序。SimpleSSLServer 和 SimpleSSLClient 是我們的演示應用程序的兩個組件。爲了運行示例,需要在應用程序的每一端上設置好幾個 KeyStore 和 TrustStore 文件。特別是您將需要:
- 稱爲 clientKeys 的用於客戶機的 KeyStore 文件,分別包含用於虛構的通信者 Alice 和 Bob 的證書
- 稱爲 serverKeys 的用於服務器的 KeyStore 文件,包含一個用於服務器的證書
- 稱爲 clientTrust 的用於客戶機的 TrustStore 文件,包含服務器的證書
- 稱爲 serverTrust 的用於服務器的 TrustStore 文件,包含 Alice 和 Bob 的證書
接下來,下載本文隨附的 jar 文件。這些文件包含客戶機/服務器應用程序的源代碼和已編譯的版本,因此,只要把它們放到 CLASSPATH 中,就可以使用了。
要運行 SimpleSSLServer,我們輸入如下(稍微冗長的)命令:
java -Djavax.net.ssl.keyStore=serverKeys -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStore=serverTrust -Djavax.net.ssl.trustStorePassword=password SimpleSSLServer |
可以看到,我們已指定了 KeyStore,用它來標識服務器,還指定了在 KeyStore 中設置的密碼。由於服務器將需要客戶機認證,因此我們也爲它提供了 TrustStore。通過指定 TrustStore,我們確保 SSLSimpleServer 將信任由 SSLSimpleClient 提供的證書。服務器初始化它自己後,您將得到下述報告:
SimpleSSLServer running on port 49152 |
之後,服務器將等待來自客戶機的連接。如果希望在另一個端口上運行服務器,在命令的結尾處指定 -port xxx
,用選定的端口代替變量 xxx
。
接下來,設置應用程序的客戶機組件。從另一個控制檯窗口輸入如下命令:
java -Djavax.net.ssl.keyStore=clientKeys -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStore=clientTrust -Djavax.net.ssl.trustStorePassword=password SimpleSSLClient |
缺省情況下,客戶機將嘗試連接到運行在本地主機端口 49152 上的服務器。可以在命令行上使用 -host
和 -port
參數更改主機。當客戶機已連接到服務器時,您會得到消息:
Connected |
與此同時,服務器將報告連接請求,並顯示由客戶機提供的區別名,如下所示:
1: New connection request 1: Request from CN=Bob, OU=developerWorks, O=IBM, L=Winchester, ST=Hampshire, C=UK |
爲了測試新連接,試着向客戶機輸入一些文本,按 Return 鍵,並觀察服務器是否回顯文本。要殺死客戶機,在客戶機控制檯上按 Ctrl-C 鍵。服務器將如下所示記錄斷開連接:
1: Client disconnected |
無需殺死服務器;在各種練習過程中我們只需保持服務器運行。
儘管本文餘下部分主要都是講述客戶機應用程序的,但是,查看一下服務器代碼仍然是很值得的。除了可以瞭解服務器應用程序是如何工作外,您還可以學會如何使用 HandshakeCompletedListener
接口檢索有關 SSL 連接的信息。
SimpleSSLServer.java
從三條 import 語句開始,如下所示:
import javax.net.ssl.*; import java.security.cert.*; import java.io.*; |
javax.net.ssl
是三條語句中最重要的;它包含大多數核心 JSSE 類,我們要用它處理任何和 SSL 有關的工作。java.security.cert
在您需要操作單獨的證書(在本文後面我們將這樣做)時很有用。java.io
是標準的 Java I/O 包。在本案例中,我們將使用它來處理通過安全套接字接收和發送的數據。
接下來, main()
方法檢查命令行中可選的 -port
參數。然後它獲取缺省的 SSLServerSocketFactory
,構造一個 SimpleSSLServer
對象,把工廠(factory)傳遞給構造器,並且啓動服務器,如下所示:
SSLServerSocketFactory ssf= (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SimpleSSLServer server=new SimpleSSLServer(ssf, port); server.start(); |
由於服務器是在單獨的線程上運行的,因此只要啓動並運行, main()
就退出。新的線程調用 run()
方法,這樣會創建一個 SSLServerSocket
,並且設置服務器以要求客戶機認證,如下所示:
SSLServerSocket serverSocket= (SSLServerSocket)serverSocketFactory.createServerSocket(port); serverSocket.setNeedClientAuth(true); |
將它激活之後, run()
方法進行無限循環,等待來自客戶機的請求。每個套接字都與 HandshakeCompletedListener
實現相關聯,後者用來顯示來自客戶機證書的區別名(DN)。套接字的 InputStream
封裝在一個 InputDisplayer
中,它作爲另一個線程運行,並且將來自套接字的數據回顯到 System.out
。SimpleSSLServer 的主循環如清單 1 所示:
while (true) { String ident=String.valueOf(id++); // Wait for a connection request. SSLSocket socket=(SSLSocket)serverSocket.accept(); // We add in a HandshakeCompletedListener, which allows us to // peek at the certificate provided by the client. HandshakeCompletedListener hcl=new SimpleHandshakeListener(ident); socket.addHandshakeCompletedListener(hcl); InputStream in=socket.getInputStream(); new InputDisplayer(ident, in); } |
我們的 HandshakeCompletedListener
― SimpleHandshakeListener
提供了一個 handshakeCompleted()
方法的實現。當 SSL 握手階段完成時,該方法由 JSSE 基礎設施調用,並且傳遞(在 HandshakeCompletedEvent
對象中)有關連接的信息。我們使用這個方法獲取並顯示客戶機的 DN,如清單 2 所示:
class SimpleHandshakeListener implements HandshakeCompletedListener { String ident; /** * Constructs a SimpleHandshakeListener with the given * identifier. * @param ident Used to identify output from this Listener. */ public SimpleHandshakeListener(String ident) { this.ident=ident; } /** Invoked upon SSL handshake completion. */ public void handshakeCompleted(HandshakeCompletedEvent event) { // Display the peer specified in the certificate. try { X509Certificate cert=(X509Certificate)event.getPeerCertificates()[0]; String peer=cert.getSubjectDN().getName(); System.out.println(ident+": Request from "+peer); } catch (SSLPeerUnverifiedException pue) { System.out.println(ident+": Peer unverified"); } } } |
|
用紅色突出顯示的行是非常重要的兩行: getPeerCertificates
返回作爲 X509Certificate
對象數組的證書鏈。這些證書對象建立對等方的(即客戶機的)標識。數組中的第一個是客戶機本身的證書;最後一個通常是 CA 證書。一旦我們擁有了對等方的證書,我們可以獲取 DN 並將其顯示到 System.out
。 X509Certificate
是在包 java.security.cert
中定義的。