天天看點

SSL雙向認證建構安全的Socket為進階 JSSE 開發人員定制 SSL

 轉載位址:http://blog.csdn.net/yangdelong/article/details/4575983

SSL(安全套接層)是 Netscape公司在1994年開發的,最初用于WEB浏覽器,為浏覽器與伺服器間的資料傳遞提供安全保障,提供了加密、來源認證和資料完整性的功能。現在SSL3.0得到了普遍的使用,它的改進版TLS(傳輸層安全)已經成為網際網路标準。SSL本身和TCP套接字連接配接是很相似的,在協定棧中,SSL可以被簡單的看作是安全的TCP連接配接,但是某些TCP連接配接的特性它是不支援的,比如帶外資料(out-of-bound)。

    在建構基于 Socket的C/S程式時,通過添加對SSL的支援來保障資料安全和完整是不錯的方法。完善的Java為我們提供了簡單的實作方法:JSSE(Java 安全套接字擴充)。JSSE是一個純Java實作的SSL和TLS協定架構,抽象了SSL和TLS複雜的算法,使安全問題變得簡單。JSSE已經成為 J2SE1.4版本中的标準元件,支援SSL 3.0和TLS 1.0。我們将通過一個具體的例子示範JSSE的一些基本應用。例子中的伺服器端将打開一個SSL Socket,隻有持有指定證書的用戶端可以與它連接配接,所有的資料傳遞都是加密的。

    構造一個SSLSocket是非常簡單的:

    SSLServerSocketFactory factory=(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();

    SSLServerSocket server = (SSLServerSocket) factory.createServerSocket(portNumber);

    SSLSocket socket = (SSLSocket);

    但是執行這樣的程式會産生一個異常,報告找不到可信任的證書。SSLSocket和普通的Socket是不一樣的,它需要一個證書來進行安全認證。

    一、 證書

    生成一個CA憑證,在指令行下執行:

    keytool –genkey –keystore SSLKey –keyalg rsa –alias SSL

    黑體部分是使用者可以自己指定的參數,第一個參數是要生成的證書的名字,第二個參數是證書的别名。rsa指明了我們使用的加密方法。

    系統會要求輸入證書發放者的資訊,逐項輸入即可

    系統生成的檔案命将會和證書名相同。證書可以送出給權威CA認證組織稽核,如果通過稽核,組織會提供信任擔保,向客戶擔保你的連接配接是安全的。當然這不是必須的。在我們的例子中會把證書直接打包到用戶端程式中,保證用戶端是授權使用者,避免僞造客戶,是以不需要送出稽核。

    二、 伺服器端

    現在可以編寫伺服器端的代碼,與普通的Socket代碼不同,我們需要在程式中導入證書,并使用該證書構造SSLSocket。需要的說明的是:

    ●KeyStore ks=KeyStore.getInstance("JKS");

    通路Java密鑰庫,JKS是keytool建立的Java密鑰庫,儲存密鑰。

    ● KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509");

    建立用于管理JKS密鑰庫的X.509密鑰管理器。

    ● SSLContext sslContext=SSLContext.getInstance("SSLv3");

    構造SSL環境,指定SSL版本為3.0,也可以使用TLSv1,但是SSLv3更加常用。

    ●sslContext.init(kmf.getKeyManagers(),null,null);

    初始化SSL環境。第二個參數是告訴JSSE使用的可信任證書的來源,設定為null是從javax.net.ssl.trustStore中獲得證書。第三個參數是JSSE生成的随機數,這個參數将影響系統的安全性,設定為null是個好選擇,可以保證JSSE的安全性。

    完整代碼如下:

    package org.ec107.ssl;

    import java.net.*;

    import javax.net.ssl.*;

    import java.io.*;

    import java.security.*;

    public class SSLServer

    {

    static int port=8266;  //系統将要監聽的端口号,82.6.6是偶以前女朋友的生日^_^

    static SSLServerSocket server;

    public SSLServer()

    {

    }

    private static SSLServerSocket getServerSocket(int thePort)

    {

    SSLServerSocket s=null;

    try

    {

    String key="SSLKey";  //要使用的證書名

    char keyStorePass[]="12345678".toCharArray();  //證書密碼

    char keyPassword[]="12345678".toCharArray();  //證書别稱所使用的主要密碼

    KeyStore ks=KeyStore.getInstance("JKS");  //建立JKS密鑰庫

    ks.load(new FileInputStream(key),keyStorePass);

    //建立管理JKS密鑰庫的X.509密鑰管理器

    KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509");

    kmf.init(ks,keyPassword);

    SSLContext sslContext=SSLContext.getInstance("SSLv3");

    sslContext.init(kmf.getKeyManagers(),null,null);

    //根據上面配置的SSL上下文來産生SSLServerSocketFactory,與通常的産生方法不同

    SSLServerSocketFactory factory=sslContext.getServerSocketFactory();

    s=(SSLServerSocket)factory.createServerSocket(thePort);

    }catch(Exception e)

    {

    System.out.println(e);

    }

    return(s);

    }

    public static void main(String args[])

    {

    try

    {

    server=getServerSocket(port);

    System.out.println("在”+port+”端口等待連接配接...");

    while(true)

    {

    SSLSocket socket=(SSLSocket)server.accept();

    //将得到的socket交給CreateThread對象處理,主線程繼續監聽

    new CreateThread(socket);

    }

    }catch(Exception e)

    {

    System.out.println("main方法錯誤80:"+e);

    }

    }

    }

    class CreateThread extends Thread

    {

    static BufferedReader in;

    static PrintWriter out;

    static Socket s;

    public CreateThread(Socket socket)

    {

    try

    {

    s=socket;

    in=new BufferedReader(new InputStreamReader(s.getInputStream(),"gb2312"));

    out=new PrintWriter(s.getOutputStream(),true);

    start();  //開新線程執行run方法

    }catch(Exception e)

    {

    System.out.println(e);

    }

    }

    public void run()

    {

    try

    {

    String msg=in.readLine();

    System.out.println(msg);

    s.close();

    }catch(Exception e)

    {

    System.out.println(e);

    }

    }

    }

    将我們剛才生成的證書放到程式所在的目錄下,上面的代碼就可以在編譯之後執行:

    java org.ec107.ssl.SSLServer

    在8266端口等待連接配接…

    三、 用戶端

    用戶端的代碼相對簡單,我們可以不在程式中指定SSL環境,而是在執行用戶端程式時指定。需要注意的是用戶端并沒有導入證書,而是采用了預設的工廠方法構造SSLSocket:

    ● SSLSocketFactory factory=(SSLSocketFactory)SSLSocketFactory.getDefault();

    構造預設的工廠方法

    ●Socket s=factory.createSocket("localhost",port);

    打開一個SSLSocket連接配接

    package org.ec107.ssl;

    import java.net.*;

    import javax.net.ssl.*;

    import javax.net.*;

    import java.io.*;

    public class SSLClient

    {

    static int port=8266;

    public static void main(String args[])

    {

    try

    {

    SSLSocketFactory factory=(SSLSocketFactory)SSLSocketFactory.getDefault();

    Socket s=factory.createSocket("localhost",port);

    PrintWriter out=new PrintWriter(s.getOutputStream(),true);

    out.println("安全的說你好");

    out.close();

    s.close();

    }catch(Exception e)

    {

    System.out.println(e);

    }

    }

    }

    把伺服器産生的證書(SSLKey)拷貝到程式所在的目錄,執行這個程式的時候需要向javax.net.ssl.trustStore環境變量傳入證書名:

    java –Djavax.net.ssl.trustStore=SSLKey org.ec107.ssl.SSLClient

    可以在伺服器的控制台看到用戶端發送過來的資料。

    執行用戶端可以有另一種方法,把證書拷貝到java home/lib/security目錄下,名字改為jssecacerts,然後可以直接執行用戶端:

    java org.ec107.ssl.SSLClient

    程式會自動的到上述目錄下去尋找jssecacerts檔案作為預設的證書。需要注意的是這裡的java home并不是我們在安裝J2SE時指定的那個JAVA_HOME。可以執行一個程式來得到java home的位置:

     public class GetJavaHome

    {

    public static void main(String args[])

    {

    System.out.println(System.getProperty(“java.home”));

    }

    }

    一般情況下(windows 2K)hava home的位置是在C:Program FilesJavaj2re1.4.0_02,相對的,證書就應該拷貝到C:Program FilesJavaj2re1.4.0_02libsecurity下,如果安裝了自帶JDK的Java IDE,比如 JBuilder,情況可能會有不同。

    如果程式客戶在不持有證書的情況下直接進行連接配接,伺服器端會産生運作時異常,不允許進行連接配接。

    運作環境:windows 2K server,j2sdk1.4.1

所謂認證,是要對某台(當然可以是叢集)伺服器身份做認證.

認證方式有兩種:

自簽名認證:服務端生成key,然後根據key導出證書.公布于站點.

通過第三方認證機構認證:有服務端生成key,然後導出認證資訊,交由天威誠信等第三方認證機構認證,最後生成證書,公布于站點.

用戶端,将證書下載下傳,确認為可信任公司認證資訊,并且導入到受信任區(trustscore),建立連接配接與服務端進行正常互動.至此,就完成了對服務端的認證.

是以用戶端必須将證書導入信任區.

java中,可以檢視證書:

寫個簡單的方法:

FileInputStream fis = new FileInputStream("cert.cer");

CertificateFactory cf=CertificateFactory.getInstance("X509");

X509Certificate c=(X509Certificate) cf.generateCertificate(fis);

System.out.println(c.getSubjectDN());

//可以檢視下X509Certificate的一些get方法.

其實keytool僅僅是jdk中的工具.

openssl是更常用的一個工具

SSL雙向認證java實作

http://www.blogjava.net/stone2083/archive/2008/07/10/169015.html

本文通過模拟場景,介紹SSL雙向認證的java實作

預設的情況下,我認為讀者已經對SSL原理有一定的了解,是以文章中對SSL的原理,不做詳細的介紹。

如果有這個需要,那麼通過GOOGLE,可以搜尋到很多這樣的文章。

模拟場景:

Server端和Client端通信,需要進行授權和身份的驗證,即Client隻能接受Server的消息,Server隻能接受Client的消息。

實作技術:

JSSE(Java Security Socket Extension)

是Sun為了解決在Internet上的安全通訊而推出的解決方案。它實作了SSL和TSL(傳輸層安全)協定。在JSSE中包含了資料加密,伺服器驗證,消息完整性和用戶端驗證等技術。通過使用JSSE,開發人員可以在客戶機和伺服器之間通過TCP/IP協定安全地傳輸資料

為了實作消息認證。

Server需要:

1)KeyStore: 其中儲存服務端的私鑰

2)Trust KeyStore:其中儲存用戶端的授權證書

同樣,Client需要:

1)KeyStore:其中儲存用戶端的私鑰

2)Trust KeyStore:其中儲存服務端的授權證書

我們可以使用Java自帶的keytool指令,去生成這樣資訊檔案

1)生成服務端私鑰,并且導入到服務端KeyStore檔案中

keytool -genkey -alias serverkey -keystore kserver.keystore

過程中,分别需要填寫,根據需求自己設定就行

keystore密碼:123456

名字和姓氏:stone

組織機關名稱:eulic

組織名稱:eulic

城市或區域名稱:HZ

州或省份名稱:ZJ

國家代碼:CN

serverkey私鑰的密碼,不填寫和keystore的密碼一緻:123456

就可以生成kserver.keystore檔案

server.keystore是給服務端用的,其中儲存着自己的私鑰

2)根據私鑰,導出服務端證書

keytool -export -alias serverkey -keystore kserver.keystore -file server.crt

server.crt就是服務端的證書

3)将服務端證書,導入到用戶端的Trust KeyStore中

keytool -import -alias serverkey -file server.crt -keystore tclient.keystore

tclient.keystore是給用戶端用的,其中儲存着受信任的證書

采用同樣的方法,生成用戶端的私鑰,用戶端的證書,并且導入到服務端的Trust KeyStore中

1)keytool -genkey -alias clientkey -keystore kclient.keystore

2)keytool -export -alias clientkey -keystore kclient.keystore -file client.crt

3)keytool -import -alias clientkey -file client.crt -keystore tserver.keystore

如此一來,生成的檔案分成兩組

服務端儲存:kserver.keystore tserver.keystore

用戶端儲存:kclient.keystore  tclient.kyestore

接下來,就采用JSSE,分别生成SSLServerSocket,SSLSocket

服務端,生成SSLServerSocket代碼

SSLContext ctx = SSLContext.getInstance("SSL");

KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");

TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

KeyStore ks = KeyStore.getInstance("JKS");

KeyStore tks = KeyStore.getInstance("JKS");

ks.load(new FileInputStream("data/kserver.keystore"), SERVER_KEY_STORE_PASSWORD.toCharArray());

tks.load(new FileInputStream("data/tserver.keystore"), SERVER_TRUST_KEY_STORE_PASSWORD.toCharArray());

kmf.init(ks, SERVER_KEY_STORE_PASSWORD.toCharArray());

tmf.init(tks);

ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

SSLServerSocket  serverSocket = (SSLServerSocket) ctx.getServerSocketFactory().createServerSocket(DEFAULT_PORT);

serverSocket.setNeedClientAuth(true); //表明需要驗證用戶端的身份。

return serverSocket;

用戶端,生成SSLSocket的代碼,大同小異

SSLContext ctx = SSLContext.getInstance("SSL");

KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");

TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

KeyStore ks = KeyStore.getInstance("JKS");

KeyStore tks = KeyStore.getInstance("JKS");

ks.load(new FileInputStream("data/kclient.keystore"), CLIENT_KEY_STORE_PASSWORD.toCharArray());

tks.load(new FileInputStream("data/tclient.keystore"), CLIENT_TRUST_KEY_STORE_PASSWORD.toCharArray());

kmf.init(ks, CLIENT_KEY_STORE_PASSWORD.toCharArray());

tmf.init(tks);

ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

return (SSLSocket) ctx.getSocketFactory().createSocket(DEFAULT_HOST, DEFAULT_PORT);

如此,就完成了服務端和用戶端之間的基于身份認證的互動。

client采用kclient.keystore中的clientkey私鑰進行資料加密,發送給server

server采用tserver.keystore中的client.crt證書(包含了clientkey的公鑰)對資料解密,如果解密成功,證明消息來自client,進行邏輯處理

server采用kserver.keystore中的serverkey私鑰進行資料叫米,發送給client

client采用tclient.keystore中的server.crt證書(包含了serverkey的公鑰)對資料解密,如果解密成功,證明消息來自server,進行邏輯處理

如果過程中,解密失敗,那麼證明消息來源錯誤。不進行邏輯處理。這樣就完成了雙向的身份認證。

下面我附上簡單的SSLServer.java SSLClient.java,供大家示範用。

啟動服務端的時候,大家不妨采用telnet 127.0.0.1 7777連接配接,看看能不能實作消息傳遞。

ssl demo

受限于自己對jsse了解非常的淺,上面的文章僅僅是覆寫了jsse很表層的内容。

推薦ibm網站上的一篇文章,對jsse和ssl寫得很深入淺出。

為進階 JSSE 開發人員定制 :http://www.ibm.com/developerworks/cn/java/j-customssl/

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);
      

HandshakeCompletedListener

将它激活之後,

run()

方法進行無限循環,等待來自客戶機的請求。每個套接字都與

HandshakeCompletedListener

實作相關聯,後者用來顯示來自客戶機證書的差別名(DN)。套接字的

InputStream

封裝在一個

InputDisplayer

中,它作為另一個線程運作,并且将來自套接字的資料回顯到

System.out

。SimpleSSLServer 的主循環如清單 1 所示:

清單 1. SimpleSSLServer 主循環

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 所示:

清單 2. SimpleHandshakeListener

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");
    }
  }
}
      

在 J2SE 1.2 或 1.3 上運作伺服器應用程式

如果您正在 J2SE 1.2 或 1.3 上運作 SimpleSSLServer 應用程式,則需要使用一個略微不同的(并且目前已過時的)JSSE API。不是導入

java.security.cert

,而是使用

javax.security.cert

。該包還包含稱為

X509Certificate

的類;但是,為了從

HandshakeCompletedEvent

獲得證書對象數組,必須使用

getPeerCertificateChain()

方法,而不是

getPeerCertificates()

方法。

用紅色突出顯示的行是非常重要的兩行:

getPeerCertificates

傳回作為

X509Certificate

對象數組的證書鍊。這些證書對象建立對等方的(即客戶機的)辨別。數組中的第一個是客戶機本身的證書;最後一個通常是 CA 證書。一旦我們擁有了對等方的證書,我們可以擷取 DN 并将其顯示到

System.out

X509Certificate

是在包

java.security.cert

中定義的。

我們将研究的第一個客戶機應用程式根本不能做什麼。但是,在後面的示例中我們會擴充它來闡述更進階的功能。設定 SimpleSSLClient 的目的是為了友善地添加子類。打算覆寫下面四個方法:

  • main()

    當然是在從指令行運作類時被調用。對于每個子類,

    main()

    必須構造一個合适類的對象,并調用對象上的

    runClient()

    close()

    方法。這些方法是在超類 ―

    SimpleSSLClient

    上提供的,并且不打算被覆寫。
  • handleCommandLineOption()

    displayUsage()

    允許每個子類在指令行上添加選項,而無需更新父類。它們都從

    runClient()

    方法調用。
  • getSSLSocketFactory()

    是一個有趣的方法。JSSE 安全套接字始終是從

    SSLSocketFactory

    對象構造的。通過構造一個定制的套接字工廠,我們可以定制 JSSE 的行為。為了将來練習的目的,每個 SimpleSSLClient 子類都實作該方法,并相應定制

    SSLSocketFactory

目前,SimpleSSLClient 僅能了解

-host

-port

參數,這允許使用者把客戶機指向伺服器。在這第一個基本示例中,

getSSLSocketFactory

傳回(JVM 範圍的)預設工廠,如下所示:

protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  return (SSLSocketFactory)SSLSocketFactory.getDefault();
}
      

從子類的

main()

方法調用的

runClient()

方法,負責處理指令行參數,然後從子類擷取

SSLSocketFactory

來使用。然後它使用

connect()

方法連接配接到伺服器,并且使用

transmit()

方法在安全通道上開始傳輸資料。

connect()

方法相當簡單。在使用

SSLSocketFactory

連接配接到伺服器之後,它調用安全套接字上的

startHandshake

。這迫使 JSSE 完成 SSL 握手階段,并因而觸發伺服器端上的

HandshakeCompletedListener

。盡管 JSSE 确實會自動啟動握手,但是僅當資料首次通過套接字發送時它才這樣做。因為使用者在鍵盤上輸入消息之前我們不會發送任何資料,但是我們希望伺服器立即報告連接配接,是以我們需要使用

startHandshake

強制進行握手。

transmit()

方法同樣相當簡單。它的首要任務把輸入源包裝到适當的

Reader

,如下所示:

BufferedReader reader=new BufferedReader(
  new InputStreamReader(in));
      

我們使用

BufferedReader

,因為它将幫我們把輸入分割成單獨的行。

接下來,

transmit()

方法把輸出流 ― 在本案例中,由安全套接字提供

OutputStream

― 包裝到适當的

Writer

中。伺服器希望文本是以 UTF-8 編碼的,是以我們可以讓

OutputStreamWriter

使用下列編碼:

writer=new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
      

主循環很簡單;正如您在清單 3 中看到的,它看起來更象 SimpleSSLServer 中

InputDisplayer

的主循環:

清單 3. SimpleSSLClient 主循環

boolean done=false;
while (!done) {
  String line=reader.readLine();
  if (line!=null) {
    writer.write(line);
    writer.write('\n');
    writer.flush();
  }
  else done=true;
}
      

基本的 JSSE 伺服器和客戶機代碼就隻有這些。現在,我們可以繼續擴充 SimpleSSLClient,并且看看一些其它

getSSLSocketFactory

實作。

自制的 KeyStore

還記得我們是如何運作 SimpleSSLClient 的嗎?指令如下:

java -Djavax.net.ssl.keyStore=clientKeys
   -Djavax.net.ssl.keyStorePassword=password 
   -Djavax.net.ssl.trustStore=clientTrust 
   -Djavax.net.ssl.trustStorePassword=password SimpleSSLClient
      

指令簡直太長了!幸運的是,該示例及接下來的示例将為您示範如何設定一個帶有到 KeyStore 和 TrustStore 的寫死路徑的

SSLSocketFactory

。除了減少上述指令的長度之外,您将學習的技術将允許您設定多個

SSLSocketFactory

對象,每個對象都帶有不同的 KeyStore 和 TrustStore 設定。如果沒有這種配置,JVM 中的每個安全連接配接必須使用相同的 KeyStore 和 TrustStore。盡管對于較小的應用程式而言這是可以接受的,但是較大的應用程式可能需要連接配接到多個代表許多不同使用者的對等方。

介紹 CustomKeyStoreClient

對于第一個示例,我們将使用示例應用程式 CustomKeyStoreClient(可在本文的源代碼中找到)來動态定義一個 KeyStore。在研究源代碼之前,讓我們看看正在使用的 CustomKeyStoreClient。對于這個練習,我們将指定 TrustStore 而不是 KeyStore。在 CustomKeyStoreClient 指令行上輸入下列參數,我們将看到出現的結果:

java -Djavax.net.ssl.trustStore=clientTrust
   -Djavax.net.ssl.trustStorePassword=password CustomKeyStoreClient
      

假定客戶機連接配接良好,伺服器将報告說提供的證書是有效的。連接配接成功,因為

CustomKeyStoreClient.java

已經寫死了 KeyStore 的名稱(

clientKeys

)和密碼(

password

)。如果您為客戶機 KeyStore 選擇了另外的檔案名或密碼,那麼可以使用新的指令行選項

-ks

-kspass

來指定它們。

研究一下

CustomKeystoreClient.java

的源代碼,

getSSLSocketFactory

做的第一件事是調用助手方法

getKeyManagers()

。稍後我們将考慮這是如何工作的;目前隻是注明它傳回

KeyManager

對象數組,已經利用必需的 KeyStore 檔案和密碼對其進行了設定。

清單 4. CustomKeyStoreClient.getSSLSocketFactory

protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  // Call getKeyManagers to get suitable key managers
  KeyManager[] kms=getKeyManagers();
  // Now construct a SSLContext using these KeyManagers. We
  // specify a null TrustManager and SecureRandom, indicating that the
  // defaults should be used.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, null, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}
      

獲得

KeyManager

數組之後,

getSSLSocketFactory

執行一些對所有 JSSE 定制通常都很重要的設定工作。為了構造

SSLSocketFactory

,應用程式擷取一個

SSLContext

執行個體,對其進行初始化,然後使用

SSLContext

生成一個

SSLSocketFactory

當得到

SSLContext

時,我們指定

"SSL"

的協定;我們也可以在這放入特定的 SSL(或 TLS)協定版本,并且強制通信在特定的級别發生。通過指定

"SSL"

,我們允許 JSSE 預設至它能支援的最進階别。

SSLContext.init

的第一個參數是要使用的

KeyManager

數組。第二個參數(這裡保留為 null)類似于

TrustManager

對象數組,稍後我們将使用它們。通過讓第二個參數為 null,我們告訴 JSSE 使用預設的 TrustStore,它從

javax.net.ssl.trustStore

javax.net.ssl.trustStorePassword

系統屬性挑選設定。第三個參數允許我們覆寫 JSSE 的随機數生成器(RNG)。RNG 是 SSL 的一個敏感領域,誤用該參數會緻使連接配接變得不安全。我們讓該參數為 null,這樣允許 JSSE 使用預設的 ― 并且安全的!―

SecureRandom

對象。

裝入 KeyStore

接下來,我們将研究

getKeyManagers

如何裝入和初始化

KeyManagers

數組。先從清單 5 中的代碼開始,然後我們将讨論正在發生什麼。

清單 5. 裝入和初始化 KeyManagers

protected KeyManager[] getKeyManagers()
  throws IOException, GeneralSecurityException
{
  // First, get the default KeyManagerFactory.
  String alg=KeyManagerFactory.getDefaultAlgorithm();
  KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg);
    
  // Next, set up the KeyStore to use. We need to load the file into
  // a KeyStore instance.
  FileInputStream fis=new FileInputStream(keyStore);
  KeyStore ks=KeyStore.getInstance("jks");
  ks.load(fis, keyStorePassword.toCharArray());
  fis.close();
  // Now we initialize the TrustManagerFactory with this KeyStore
  kmFact.init(ks, keyStorePassword.toCharArray());
  // And now get the TrustManagers
  KeyManager[] kms=kmFact.getKeyManagers();
  return kms;
}
      

首要工作是擷取

KeyManagerFactory

,但是要這樣做,我們需要知道将使用哪種算法。幸運的是,JSSE 使預設的

KeyManagerFactory

算法可用。可以使用

ssl.KeyManagerFactory.algorithm

安全性屬性配置預設算法。

接下來,

getKeyManagers()

方法裝入 KeyStore 檔案。這其中包括從檔案建立一個

InputStream

、擷取一個

KeyStore

執行個體,以及從

InputStream

裝入

KeyStore

。除了

InputStream

KeyStore

需要知道流的格式(我們使用預設的

"jks"

)和存儲密碼。存儲密碼必須作為字元數組提供。

CustomKeyStoreClient 包導入

為了通路

KeyStore

類,我們必須導入

javax.net.ssl

java.security.cert

。其它類(如

SSLContext

KeyManagerFactory

)從 J2SE 1.4 起,是

javax.net.ssl

的成員。在 J2SE 1.2 或 1.3 中,這些類的位置不是标準的;例如,Sun JSSE 實作把它們放在

com.sun.net.ssl

中。

要說明的一個可能很有用的竅門是,

KeyStore.load

會擷取任何

InputStream

。您的應用程式可以從任何地方建構這些流;除了檔案,您可以通過網絡、從移動裝置擷取流,或者甚至直接生成流。

裝入

KeyStore

之後,我們使用它來初始化以前建立的

KeyManagerFactory

。我們需要再次指定一個密碼,這次是單獨的證書密碼。通常,對于 JSSE 而言,KeyStore 中的每個證書都需要具備與 KeyStore 本身相同的密碼。自己構造

KeyManagerFactory

可以克服這個限制。

KeyManagerFactory

初始化之後,它通常使用

getKeyManagers()

方法擷取相應的

KeyManager

對象的數組。

對于 CustomKeyStoreClient 而言,我們已經研究了如何從任意的位置(本文使用本地檔案系統)裝入 KeyStore,以及如何讓證書和 KeyStore 本身使用不同的密碼。稍後我們将研究如何允許 KeyStore 中的每個證書擁有不同的密碼。盡管在本示例中我們着重于客戶機端,但是,我們可以在伺服器端使用相同的技術來建構适當的

SSLServerSocketFactory

對象。

CustomTrustStoreClient 包導入

同樣,本示例中使用的類會出現在不同 JSSE 供應商的不同包中。在 J2SE 1.4 中,

TrustManagerFactory

位于

javax.net.ssl

中;在 J2SE 1.2 或 1.3 中,通常它位于

com.sun.net.ssl

中。

使用您自己的 TrustStore

覆寫 JSSE 的預設 TrustStore 非常類似于我們剛才用 KeyStore 所做的工作,這并不令人驚奇。我們已經設定了 CustomTrustStoreClient(可在本文的源代碼中找到)來使用寫死的 KeyStore 和寫死的 TrustStore。開始運作它所需做的全部工作就是輸入指令:

java CustomTrustStoreClient

CustomTrustStoreClient 希望 KeyStore 将是一個名為

clientKeys

并且密碼為

password

的檔案。希望 TrustStore 将是一個名為

clientTrust

,密碼為

password

的檔案。就象使用 CustomKeyStoreClient 一樣,可以使用

-ks

-kspass

-ts

-tspass

參數覆寫這些預設值。

getSSLSocketFactory()

在許多方面與 CustomKeyStoreClient 中相同方法是一樣的。我們甚至從 CustomKeyStoreClient 調用

getKeyManagers()

方法來擷取與前面示例中相同的定制的

KeyManager

對象數組。但是這時

getSSLSocketFactory

還必須擷取一個定制的

TrustManager

對象數組。在清單 6 中,我們可以看到

getSSLSocketFactory

如何使用助手方法

getTrustManagers()

擷取定制的

TrustManager

對象:

清單 6. getSSLSocketFactory 如何使用 TrustManagers

protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  // Call getTrustManagers to get suitable trust managers
  TrustManager[] tms=getTrustManagers();
    
  // Call getKeyManagers (from CustomKeyStoreClient) to get suitable
  // key managers
  KeyManager[] kms=getKeyManagers();
  // Next construct and initialize a SSLContext with the KeyStore and
  // the TrustStore. We use the default SecureRandom.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, tms, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}
      

這時,當初始化上下文時,我們覆寫了 KeyStore 和 TrustStore。但是,我們仍然讓 JSSE 通過傳遞

null

作為第三個參數來使用它預設的

SecureRandom

getTrustManagers

也非常類似于 CustomKeyStoreClient 的等價物同樣不足為奇,如清單 7 所示:

清單 7. 裝入和初始化 TrustManagers

protected TrustManager[] getTrustManagers()
  throws IOException, GeneralSecurityException
{
  // First, get the default TrustManagerFactory.
  String alg=TrustManagerFactory.getDefaultAlgorithm();
  TrustManagerFactory tmFact=TrustManagerFactory.getInstance(alg);
    
  // Next, set up the TrustStore to use. We need to load the file into
  // a KeyStore instance.
  FileInputStream fis=new FileInputStream(trustStore);
  KeyStore ks=KeyStore.getInstance("jks");
  ks.load(fis, trustStorePassword.toCharArray());
  fis.close();
  // Now we initialize the TrustManagerFactory with this KeyStore
  tmFact.init(ks);
  // And now get the TrustManagers
  TrustManager[] tms=tmFact.getTrustManagers();
  return tms;
}
      

就象以前一樣,

getTrustManagers()

方法首先根據預設算法執行個體化一個

TrustManagerFactory

。然後将 TrustStore 檔案裝入

KeyStore

對象 ― 是的,命名不大恰當 ― 并且初始化

TrustManagerFactory

跟 KeyStore 等價物不同,請注意,當初始化

TrustManagerFactory

時,無需提供密碼。不象私鑰,可信的證書無需利用單獨的密碼進行保護。

到目前為止,我們已經研究了如何動态地覆寫 KeyStore 和 TrustStore。到這兩個示例都完成時,您應該非常清楚如何設定

KeyManagerFactory

TrustManagerFactory

,并使用這些來“播種”一個

SSLContext

。最後的示例有點煩瑣:我們将建構自己的

KeyManager

實作。

定制 KeyManager 設定:選擇别名

當運作客戶機應用程式的以前版本時,您是否注意到了伺服器顯示的是哪個證書 DN?我們故意設定客戶機 KeyStore 以獲得兩個可接受的證書,一個用于 Alice,另一個用于 Bob。在這個案例中,JSSE 将選擇任何一個它認為合适的證書。在我的安裝中,似乎始終選取 Bob 的證書,但是您的 JSSE 的行為可能有所不同。

我們的示例應用程式 ― SelectAliasClient 允許您選擇提供哪個證書。因為我們在 Keystore 中按照别名

alice

bob

命名了每個證書,是以要選擇 Alice 的證書可輸入指令:

java SelectAliasClient -alias alice

當客戶機連接配接并且 SSL 握手完成時,伺服器将用如下所示進行響應:

1: New connection request
1: Request from CN=Alice, OU=developerWorks, O=IBM, L=Winchester, 
      ST=Hampshire, C=UK
      

(或者建立 Alice 的證書時所選的任何值)。類似地,如果選擇 Bob 的證書,請輸入:

java SelectAliasClient -alias bob

,伺服器将報告下述資訊:

2: New connection request
2: Request from CN=Bob, OU=developerWorks, O=IBM, L=Winchester, 
      ST=Hampshire, C=UK
      

定制 KeyManager 實作

為了強制選擇一個特殊的别名,我們将編寫一個

X509KeyManager

實作,

KeyManager

通常由 JSSE 使用來進行 SSL 通信。我們的實作将包含一個真正的

X509KeyManager

,并且簡單地通過它傳遞大多數的調用。它攔截的唯一方法是

chooseClientAlias()

;我們的實作檢查以便了解所需的别名有效還是無效,如果有效,則傳回它。

在 SSL 握手階段,

X509KeyManager

接口使用許多方法來檢索密鑰,然後使用它來辨別對等方。在參考資料部分可以找到所有方法的參考。下列方法對于本練習很重要:

  • getClientAliases()

    getServerAliases()

    分别為使用

    SSLSocket

    SSLServerSocket

    提供了有效的别名數組。
  • chooseClientAlias()

    chooseServerAlias()

    傳回單個有效的别名。
  • getCertificateChain()

    getPrivateKey()

    每個都把别名作為參數,并傳回有關已辨別證書的資訊。

定制 KeyManager 中的控制流

控制流的工作如下所示:

  1. JSSE 調用

    chooseClientAlias

    以發現要使用的别名。
  2. chooseClientAlias

    在真實的

    X509KeyManager

    上調用

    getClientAliases

    來發現一個有效的别名清單,以便于它能檢查所需的别名是否有效。
  3. JSSE 通過指定正确的别名調用

    X509KeyManager

    getCertificateChain

    getPrivateKey

    ,X509KeyManager 讓調用可以通路被包裝的 KeyManager。

KeyManager

AliasForcingKeyManager()

chooseClientAlias()

方法實際上需要多次調用

getClientAliases()

,一次對應一個 JSSE 安裝支援的密鑰類型,如清單 8 所示:

清單 8. 強制别名的選擇

public String chooseClientAlias(String[] keyType, Principal[] issuers,
                                Socket socket)
{
  // For each keyType, call getClientAliases on the base KeyManager
  // to find valid aliases. If our requested alias is found, select it
  // for return.
  boolean aliasFound=false;
  for (int i=0; i<keyType.length && !aliasFound; i++) {
    String[] validAliases=baseKM.getClientAliases(keyType[i], issuers);
    if (validAliases!=null) {
      for (int j=0; j<validAliases.length && !aliasFound; j++) {
        if (validAliases[j].equals(alias)) aliasFound=true;
      }
    }
  }
  if (aliasFound) return alias;
  else return null;
}
      

AliasForcingKeyManager

需要

X509KeyManager

的其它五種方法的實作;這些隻是調用它們在

baseKM

上的對應部分。

目前,它仍然使用

AliasForcingKeyManager

,而不是通常的

KeyManager

。這發生在

getSSLSocketFactory

中,它首先從其它示例中調用

getKeyManagers

getTrustManagers

,但是接着将每個從

getKeyManagers

傳回的

KeyManager

封裝進一個

AliasForcingKeyManager

執行個體,如清單 9 所示:

清單 9. 封裝 X509KeyManagers

protected SSLSocketFactory getSSLSocketFactory() 
  throws IOException, GeneralSecurityException
{
  // Call the superclasses to get suitable trust and key managers
  KeyManager[] kms=getKeyManagers();
  TrustManager[] tms=getTrustManagers();
  // If the alias has been specified, wrap recognized KeyManagers
  // in AliasForcingKeyManager instances.
  if (alias!=null) {
    for (int i=0; i<kms.length; i++) {
      // We can only deal with instances of X509KeyManager
      if (kms[i] instanceof X509KeyManager)
        kms[i]=new AliasForcingKeyManager((X509KeyManager)kms[i], alias);
    }
  }
  // Now construct a SSLContext using these (possibly wrapped)
  // KeyManagers, and the TrustManagers. We still use a null
  // SecureRandom, indicating that the defaults should be used.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, tms, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}
      

KeyManager 重新打包

J2SE 1.2 和 1.3 中的

KeyManager

X509KeyManager

在 J2SE 1.4 中都從供應商特定的包中移到了

javax.net.ssl

中;當接口移動時,

X509KeyManager

方法說明會略微發生一點變化。

可以使用本文探讨的技術覆寫

KeyManager

的任何方面。類似地,可以使用它們代替

TrustManager

,更改 JSSE 的機制以決定是否信任從遠端對等方流出的證書。

結束語

本文已經讨論了相當多的技巧和技術,是以讓我們以快速回顧來結束本文。現在您應當基本了解如何:

  • 使用

    HandshakeCompletedListener

    收集有關連接配接的資訊
  • SSLContext

    擷取

    SSLSocketFactory

  • 使用定制、動态的 TrustStore 或 KeyStore
  • 放寬 KeyStore 密碼與單個證書密碼必須比對的 JSSE 限制
  • 使用您自己的 KeyManager 強制選擇辨別證書

在适當的地方,我還建議擴充這些技術以用于各種應用程式案例。在您自己的實作中封裝

X509KeyManager

的技巧可用于 JSSE 中的許多其它類,當然,利用

TrustStore

KeyStore

可以做更有趣的事情,而不隻是裝入寫死的檔案。

不管您如何選擇實作本文示範的進階 JSSE 定制,任何一個都不是随便就可以實作的。在調整 SSL 内部機理的時候,請牢記:一個錯誤就會緻使您連接配接變得不安全,這很重要。

為進階 JSSE 開發人員定制 SSL

使用 JSSE 定制 SSL 連接配接的屬性

Ian Parkinson ( [email protected]), 軟體工程師, WebSphere MQ,IBM Ian Parkinson 從牛津大學畢業後,于 1998 加入了 IBM。在從事了一段大型機 z/OS 系統的程式設計工作後,目前他從事 IBM 的 WebSphere MQ 消息傳遞産品的 Java 接口工作。他在英國的 Hursley 實驗室工作。Ian 因絕對不同意“Java 太慢”而出名。可以通過 [email protected]與他聯系。

簡介: 當資料在網絡上傳播的時候,通過使用 SSL 對其進行加密和保護,JSSE 為 Java 應用程式提供了安全的通信。在本篇有關該技術的進階研究中,Java 中間件開發人員 Ian Parkinson 深入研究了 JSSE API 較不為人知的方面,為您示範了如何圍繞 SSL 的一些限制進行程式設計。您将學習如何動态地選擇 KeyStore 和 TrustStore、放寬 JSSE 的密碼比對要求,以及建構您自己定制的 KeyManager 實作。

參考資料

  • 您可以參閱本文在 developerWorks 全球站點上的 英文原文.
  • 下載下傳本文中使用 J2SE 1.4 的 源代碼。
  • 如果您使用 J2SE 1.2 或 1.3 下的 Sun JSSE 1.0.3,請下載下傳修訂的文章 源代碼。
  • 教程“ Using JSSE for secure socket communication”(developerWorks,2002 年 4 月)是對 JSSE 的很好介紹。
  • JavaWorld也提供了一篇有關 JSSE 的優秀的介紹性文章“ Build secure network applications with SSL and the JSSE API”(2001 年 5 月)。
  • “ Implement HTTPS tunneling with JSSE”是另一篇出自JavaWorld 的很棒的文章,示範了如何使用 HTTP 和

    HttpsUrlConnection

    類通過防火牆建立 HTTPS 通道。
  • ONJava.com 的“ Secure your sockets with JSSE”(2001 年 5 月)示範了如何使用 JSSE 開發安全的 Web 伺服器和客戶機。
  • 請參閱有關

    X509KeyManager

    的 Sun javadoc,擷取所有方法的完整讨論。
  • “ Security challenges for Enterprise Java in an e-business environment”出自IBM Systems Journal,它完整地概述了 J2EE Web 應用程式伺服器的安全性功能。
  • 當然,Sun 也提供了 JSSE 1.0.3和 JSSE for J2SE 1.4的正式 JSSE 使用者指南。
  • 在 developerWorks Java 技術專區中您可以發現許多有關 Java 程式設計各個方面的文章。