為高階 JSSE 開發人員定製 SSL(一)
當資料在網路上傳播的時候,通過使用 SSL 對其進行加密和保護,JSSE 為 Java 應用程式提供了安全的通訊。在本篇有關該技術的高階研究中,Java 中介軟體開發人員 Ian Parkinson 深入研究了 JSSE API 較不為人知的方面,為您演示瞭如何圍繞 SSL 的一些限制進行程式設計。您將學習如何動態地選擇 KeyStore 和 TrustStore、放寬 JSSE 的密碼匹配要求,以及構建您自己定製的 KeyManager 實現。
JSSE(Java 安全套接字擴充套件,Java Secure Socket Extension)使 Java 應用程式能夠在因特網上使用 SSL 安全地進行通訊。由於 developerWorks 已經提供了一篇涵蓋 JSSE 基本用法的教程(請參閱
首先,我們將開發一個非常簡單的安全客戶機/伺服器聊天應用程式。在我們構建該程式的客戶機端時,我將演示如何定製 KeyStore 和 TrustStore 檔案,以便從客戶機的檔案系統裝入它們。接著,我們將著重說明證書和標識。通過從 KeyStore 選擇不同的證書,可以將客戶機以不同的形式提供給不同的伺服器。如果您的客戶機應用程式需要連線到多個對等方,或者甚至它需要冒充不同的使用者,這項高階的功能都特別有用。
由於本文著重講述更高階的主題,因此假定您已經具備了 JSSE 的使用經驗。要執行示例,需要一個帶有正確安裝和配置 JSSE 提供程式的 Java SDK。J2SE 1.4 SDK 提供了已安裝和配置的 JSSE。如果您正在使用 J2SE 1.2 或 1.3,則需要獲取一個 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
中定義的。