客戶端
Java 安全套接擴展 (Java Secure Socket Extension, JSSE) 使 Internet 安全通信成為現實。它是 SSL 3.0 (Secure Socket Layer) 及 TLS 1.0 (Transport Layer Security,由 SSL 3.0 改善而來) 的框架和實現。這個包讓 Java 開發人員能夠開發安全的網絡應用;為基于 TCP/IP 的何應用協議,如 HTTP、FTP、Telnet、或者 NTTP,在客戶端和服務器端之間建立安全的數據通道。
在這篇文章的第一部分 (服務器端),作者已經詳細說明了 SSL 和 JSSE,并且說明了如何開發服務器端支持 SSL 應用程序。那一部分中我們開發了一個 HTTPS 服務器,這是一個非常有用的應用程序,在這一部分中同樣會用到它。
在這篇文章涉及到客戶端的內容,它首先簡述 JSSE,然后會做這樣一些事情
l在客戶端使用 JSSE API
l一步步的開發一個支持 SSL 的客戶端應用程序
l開發簡單的支持 SSL 的客戶端應用程序
l從服務器端導出證書并在客戶端導入
l開發一個支持 SSL 的網頁瀏覽器
JSSE
Java 安全套接擴展 (JSSE) 提供了 SSL 和 TLS 協議的框架及實現。JSSE 將復雜的、根本的加密算法抽象化了,這樣就降低了受到敏感或者危險的安全性攻擊的風險。正如你在本文中看到的那樣,由于它能將 SSL 無縫地結合在應用當然,使安全應用的開發變得非常簡單。JSSE 框架可以支撐許多不同的安全通信協議,如 SSL 2.0 和 3.0 以及 TLS 1.0,但是 J2SE v1.4 只實現了 SSL 3.0 和 TLS 1.0。
用 JSSE 編寫客戶端應用程序
JSSE API 提供了擴充的網絡套接字類、信用和密匙管理,以及為簡化套接字創建而設計的套接字工廠框架,以此擴充 java.security 和 java.net 兩個包。這些類都包含在 javax.net 和 javax.net.ssl 包中。
javax.net.sll.SSLSocketFactory 類是一個創建安全套接字的對象工廠??梢酝ㄟ^下面兩種方法獲得 SSLSocketFactory 的實例:
1、調用 SSLSocketFactory.getDefault 來獲得默認的工廠。默認的工廠被配置為只允許服務器端驗證 (不允許客戶端驗證)。注意許多電子商務網站不需要客戶端驗證。
2、使用指定的配置來構造一個新的工廠 (這不在本文講述的范圍內)。
建立 SSLSocketFactory 實例之后,你就可以通過 SSLSocketFactory 實例的 createSocket 方法創建 SSLSocket 對象了。這里有一個例子,該例通過 SSL 端口 443 (這是 HTTPS 的默認端口) 創建套接字并連接到 Sun 的 WWW 服務器。
// Get a Socket factory
SocketFactory factory = SSLSocketFactory.getDefault();
// Get Socket from factory
Socket socket = factory.createSocket("www.sun.com", 443);
使用低層的 SSL 套接字
現在,讓我們看一個使用低層套接字在 HTTPS 服務器上打開一個 SSL 套接字連接的完整例子。在這個例子中,打開了一個到 HTTPS 服務器的 SSL 套接字連接,并且讀入默認文檔的內容。示例代碼 1 展示了這個應用程序,其中用于打開 SSL 套接字的代碼已經加黑顯示了。你將會看到,應用程序中其余代碼就是常規的輸入/輸出流代碼。
代碼示例 1:ReadHttpsURL1
|
用這個應用程序進行實驗
1、拷貝 ReadHttpsURL1 類的代碼并粘貼到一個新文件中,將該文件改名為 ReadHttpsURL1.java,并保存在一個你指定的目錄下。
2、使用 javac 編譯 ReadHttpsURL1.java。
3、運行 ReadHttpsURL1 并提供一個域名作為參數,如:
Prompt> java ReadHttpsURL1 www.sun.com
幾秒種后,你會看到許多 HTML 代碼顯示在屏幕上。注意,即使我們提供的是域名 www.sun.com,我們打開的連接也是 https://www.sun.com,這是因為我們使用的端口號 443 是 HTTPS 的默認端口號。
再試試另一個例子,如:
Prompt> java ReadHttpsURL1 www.jam.ca
這次運行會拋出如下所示的異常,你能猜到是為什么嗎?
Exception in thread "main" javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: Couldn't find trusted certificate at com.sun.net.ssl.internal.ssl.BaseSSLSocketImpl.a(DashoA6275)
緣于一個很好的理由,它不能運行——因為遠端的服務器發送了一個客戶端不認識的證書。我在本文的第一部分提到過,當客戶端連接服務器的時候,服務器發送它的證書到客戶端請求驗證。這樣,第一個例子中,你進入了 www.sun.com,服務器的確發送了證書,但 Java 檢查了默認的證書庫并認出了這個證書是由可信任的 CA 產生的,默認情況下,Java 信任這個 CA。第二個例子中,你進入的是 www.jam.ca,那個網端的證書不是它自己產生的,就是由一個 Java 不知道的 CA 產生的,因此不受信任。
注意,如果系統時鐘沒有設置正確,那么它的時間就可能在證書的有效期之外,服務器會認為證書無效并拋出 CertificateException 異常。
為了讓示例正確運行,你得從 www.jam.ca 導入證收到 Java 信任的證書庫中。
導出和導入證書
為了解釋清楚如何輸出和輸入證書,我會使用我自己的 HTTPS 服務器。這個服務器在第一部分中討論過。然后,跟著下面的內容開始: 1、運行 HTTPS 服務器,像在第一部分中討論的那樣。 2、運行 ReadHttpsURL1:java ReadHttpsURL1 localhost。你同樣會得到上面所述的異常。 3、使用下面的 keytool 命令導出服務器證書: o 從 serverkeys 文件中導出別名為 qusay 的證書 o 將導出的證書保存在 server.cert 文件中,這個文件會由 keytool 創建 如你看到的那樣,我根據要求輸入了密碼。成功輸入密碼之后,服務器證書被成功的導出并保存在 server.cert 中。 Prompt> keytool -export -keystore serverkeys -alias qusay -file server.cert Enter keystore password: hellothere Certificate stored in file 4、將文件 server.cert 拷貝到 ReadHttpsURL1 所在的目錄。使用 keytool 創建一個新的 keystore 并將服務器的 server.cert 證書導入其中。這里的命令示例: Prompt> keytool -import -keystore trustedcerts -alias qusay -file server.cert 這個命令會產生下面那樣的輸出。它要求輸入密碼,這是一個新的密碼,用于 trustedcerts 這個 keystore 的。這個 keystore 由 keytool 創建。在輸出信息的最后,它詢問我是否愿意相信這個證書,我回答 yes。 Enter keystore password: clientpass Owner: CN=localhost, OU=Training and Consulting, O=javacourses.com, L=Toronto, ST=Ontario, C=CA Issuer: CN=localhost, OU=Training and Consulting, O=javacourses.com, L=Toronto, ST=Ontario, C=CA Serial number: 3dcf988a Valid from: Mon Nov 11 06:46:18 EST 2002 until: Sun Feb 09 06:46:18 EST 2003 Certificate fingerprints: MD5: 37:35:4D:3A:2B:7E:B5:09:A5:41:B3:FA:E4:3C:1D:C4 SHA1: CB:7C:77:36:79:A2:37:26:E2:98:61:C2:9D:10:50:69: 99:F9:B9:1B Trust this certificate? [no]: yes Certificate was added to keystore 5、現在運行 ReadHttpsURL1 并告訴它哪里能找到證書。使用下面的命令: Prompt> java -Djavax.net.ssl.trustStore=trustedcerts ReadHttpsURL1 localhost 這將會與你的 HTTPS 服務器聯接、校驗證書,如果正確,它會下載默認頁面 index.html。 注意:信任管理器負責決定遠端的證書是否值得信任。它使用下面的規則: 1、如果 javax.net.sll.trustStore 系統屬性指定了信任庫,那么信任管理器會使用提供的文件來檢查證書。如果那個系統屬性存在但指定的文件不存在,那么就沒有使用任何信任庫,會拋出一個 CertificateException 異常。 2、如果 javax.net.sll.trustStore 系統屬性沒有定義,那么它會去尋找默認的信任庫: 如果在你的 java.home 目錄的 lib/security 子目錄下存在名為 jssecacerts 的信任庫,那么使用的就是它。 如果 jssecacerts 不存在,但是 cacerts 存在 (它隨 J2SDK 一起發行,含有數量有限的可信任的基本證書),使用的就是 cacerts。 在我的 Windows 2000 客戶機中,java.home 目錄是 c:\Program File\java\jre1.4.1\lib\security,在上例中,如果你將 trustedcerts 更名為 jssecacerts 并將其移動到 lib/security 子目錄中,那么你以后就不需要在命令行指定 javax.net.ssl.trustStore 屬性了。 如果你不知道 java.home 在哪里,這里有一小段代碼可以讓你找到它:
public class FindJavaHome { public static void main(String argv[]) { System.out.println(System.getProperty("java.home")); } }
URL 類
示例代碼 1 中的 ReadHttpsURL1 使用低層的套接字打開到 SSL 服務器的連接。這樣做有一個缺點,如果不進行一番解析,我們就不能在命令行清楚的寫出像 https://www.jam.ca 這樣的 URL。這里有一個更簡單的辦法在客戶端應用程序中使用 SSL 和 JSSE。 java.net.URL 類支持 HTTPS 地址。例如,下面的代碼段創建一個 HTTPS 地址并建立一個輸入流的讀入器: URL url = new URL("https://www.sun.com"); BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); 是不是很簡單?我希望當你學習 Java 的新東西時,你能欣賞到它的美好之處。 示例代碼 1 中的 ReadHttpsURL1 可以由下面使用了 URL 類的示例代碼 2 代替: 示例代碼 2:ReadHttpsURL2.java
import java.net.*; import java.io.*; public class ReadHttpsURL2 { public static void main(String argv[]) throws Exception { if(argv.length != 1) { System.out.println("Usage: java ReadHttpsURL2 "); System.exit(0); } URL url = new URL(argv[0]); BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); String line; StringBuffer sb = new StringBuffer(); while ((line = in.readLine()) != null) { sb.append(line); } in.close(); System.out.println(sb.toString()); } }
如果你想試試 ReadHttpsURL2,執行它的命令和上面討論的類似。注意,無論如何,既然我們使用 URL 類,你就能在命令行指定 URL,包括協議的名稱。這里是一個例子:
Prompt> java ReadHttpsURL2 https://localhost
我們開發一個支持 SSL 的網頁瀏覽器作為一個完整的例子。該瀏覽器要做下面的工作:
1.用戶輸入 URL,瀏覽器能接收它。
2.瀏覽器能打開到 URL 指定主機的連接。
3.瀏覽器能發送 HTTP 命令。
4.瀏覽器會等待 HTTP/HTTPS 服務器的回應。
5.瀏覽器能接收 HTML 回應。
6.瀏覽器能解析 HTML 并顯示出頁面。
我們創建的瀏覽器要能處理任何 URL 如 HTTP、HTTPS、FTP 等。注意我使用工具類 javax.swing.text.html.HTMLEditorKit 來解析 HTML,它提供了對 HTML 3.2 的支持。
示例代碼 3 中展示了這個瀏覽器,QBrowser,的代碼。注意 QBrowser 實現了 Runnable 接口。我這樣做是因為這個瀏覽器沒有提供“停止”按鈕。
示例代碼 3:QBrowser.java
import java.io.*; import java.net.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class QBrowser implements ActionListener, Runnable { private JFrame frame; private JButton go; private JEditorPane content; private JTextField url; private JLabel statusLine; // default constructor public QBrowser () { buildBrowserInterface(); } private void buildBrowserInterface() { frame = new JFrame("Q's Browser"); // on close, exit the application using System.exit(0); frame.setDefaultCloseOperation (3); url = new JTextField("", 25); go = new JButton("Go Get It"); go.addActionListener(this); JPanel controls = new JPanel(new FlowLayout ()); controls.add(new JLabel("URL:")); controls.add(url); controls.add(go); content = new JEditorPane(); content.setEditable(false); // HTML text. Use the kit in the class javax.swing.text.html.HTMLEditorKit, which // provides support for HTML 3.2 content.setContentType("text/html"); content.setText(" Copyright (c) 2002 Qusay H. Mahmoud"); statusLine = new JLabel("Initialization Complete"); JPanel panel = new JPanel(new BorderLayout (0, 2)); frame.setContentPane(panel); panel.add(controls, "North"); panel.add(new JScrollPane (content), "Center"); panel.add(statusLine, "South"); frame.pack(); frame.setVisible(true); } /** * You cannot stop a download with QBrowser * The thread allows multiple downloads to start * concurrently in case a download freezes */ public void actionPerformed (ActionEvent event) { Thread thread = new Thread(this); thread.start(); } // this is the Thread's run method public void run () { try { String str = url.getText(); URL url = new URL(str); readURL(url); } catch (IOException ioe) { statusLine.setText("Error: "+ioe.getMessage()); showException(ioe); } } private void showException(Exception ex) { StringWriter trace = new StringWriter (); ex.printStackTrace (new PrintWriter (trace)); content.setContentType ("text/html"); content.setText ("" + ex + " } /** * The URL class is capable of handling http:// and https:// URLs */ private void readURL(URL url) throws IOException { statusLine.setText("Opening " + url.toExternalForm()); URLConnection connection = url.openConnection(); StringBuffer buffer = new StringBuffer(); BufferedReader in=null; try { in = new BufferedReader(new InputStreamReader (connection.getInputStream())); String line; while ((line = in.readLine()) != null) { buffer.append(line).append('\n'); statusLine.setText("Read " + buffer.length () + " bytes..."); } } finally { if(in != null) in.close(); } String type = connection.getContentType(); if(type == null) type = "text/plain"; statusLine.setText("Content type " + type); content.setContentType(type); content.setText(buffer.toString()); statusLine.setText("Done"); } public static void main (String[] args) { QBrowser browser = new QBrowser(); } }
" + trace + "
");
既然 QBrowser 使用 URL 類,它就可以處理 HTTP 和 HTTPS 請求。你可以使用 HTTP 和 HTTPS 地址測試 QBrowser。這里是一些測試:
1、請求 http://www.javacourses.com,你會看到如圖 1 所示的內容。 2、請求 https://www.jam.ca,結果拋出了異常。因為這個網頁服務器的證書不受信任并且不能在默認頁中找到,所以它拋出如圖 2 所示的異常。 3、請求 https://localhost,這里運行著第一部分中寫的 HttpServer。注意,如果你使用命令 java QBrowser 來運行 QBrowser,而服務器的證書導出后被導入默認文件 jssecacerts,那么應該將該文件拷貝到 java.home 目錄的 lib/security 子目錄中。如果證書被導入了其它文件,你可以使用 trustStore 選項,如:java -Djavax.net.ssl.trustStore=file QBrowser。使用其實任何一種方法,瀏覽器都會工作,并且你可以看到如圖 3 所示的默認頁面。 HttpsURLConnection 類 這個類存在于 javax.net.ssl 包中,它擴展了 java.net.HttpURLConnection,以支持 HTTPS 描述的一些特性。它能夠通過 SSL/TLS 套接字建立安全通道來請求/獲取數據。示例代碼 4 展示了一個小型客戶端,它使用 HttpsURLConnection 類從 HTTPS 服務器下載文檔。 示例代碼 4:ReadHttpsURL3.java
import java.io.*; import java.net.*; import javax.net.ssl.*; public class ReadHttpsURL3 { public static void main(String[] argv) throws Exception { URL url = new URL(argv[0]); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); connection.setDoOutput(true); BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; while ((line = in.readLine()) != null) { System.out.println(line); } in.close(); } }
圖 1:http://www.javacourses.com
圖 2:https://www.jam.ca
圖 3:https://localhost
現在試試 ReadHttpsURL3,完成上面討論的內容。注意,無論如何,既然我們使用 URL 類,你就能在命令行指定 URL,包括協議的名稱。這里是一個例子:
Prompt> java ReadHttpsURL3 https://www.sun.com
HttpsURLConnection 有一個非常有趣的特點:一旦獲得了連接,你就可以在網絡連接之前使用一些有用的參數對其進行配置,如 HostnameVerifier。HostnameVerifier 是一個接口,它申明了方法:public boolean verify (String hostname, SSLSession session)。而且,它像下面所述的那樣工作:
如果 SSL/TLS 標準主機名校驗邏輯失敗,執行過程中會調用回調類的 verify 方法?;卣{類是實現了 HostnameVerifier 接口的類。
如果回調類檢查到主機名可以接受,則允許連接,否則,連接會被終止。
回調類遵循的規則即可以是基本的驗證方法,也可以依賴其它驗證方法。這里說明了如何實現:
|
現在,可以這樣使用它:
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setHostnameVerifier(new MyVerifier());
信任管理器
一個 SSL 客戶端,如網頁瀏覽器,連接到 SSL 服務器 (如 HTTPS 服務器) 的時候,HTTPS 服務器將自己的證書鏈交給客戶端驗證。SSL 規范規定,如果在證書鏈中發現有無效的證書,客戶端應該立即終止連接。一些網頁瀏覽器,如 Netscape Communicator 和 Microsoft Internet Explorer,詢問用戶是否忽略無效的證書并繼續檢查證書鏈,以確定是否有可能驗證通過 HTTPS 服務器。使用 javax.net.sll.TrustManager 可以很好的消除這種矛盾,它是 JSSE 信任管理器的基礎接口。而這些信任管理器則是用來管理可信任的資料以及決定是否接受某個憑證的。典型的信任管理器都支持基于 X.509 的證書,它是 J2DK 的 keytool 可以管理的一個普通的證書格式。 X509TrustManager 接口 javax.net.sll.X509TrustManager 接口擴展了普通的 TrustManager 接口。使用基于 X.509 公鑰證書驗證方案時,信任管理器必須實現該接口。實現 X509TrustManager 可以創建信任管理器。這里有一個空實現:
public class MyTrustManager implements X509TrustManager { MyTrustManager() { // constructor // create/load keystore } public void checkClientTrusted( X509Certificate chain[], String authType) throws CertificatException { } public void checkServerTrusted( X509Certificate chain[], String authType) throws CertificationException { // special handling such as poping dialog boxes } public X509Certificate[] getAclearcase/" target="_blank" >cceptedIssuers() { } }
為了支持遠端套接字 X.509 證書,實現了 X509TrustManager 接口的類,其實例要傳遞給 SSLContext 對象的 init 方法。它作為 SSL 套接字工廠。換句話說,一旦創建了信任管理器且通過 init 方法將其分配給了一個 SSLSocket,以后從 SSLContext 創建的 SocketFactories 在作信任決策時將使用新的信任管理器。下面的代碼段就是個示例:
X509TrustManager xtm = new MyTrustManager()
TrustManager mytm[] = {xtm};
SSLContext ctx = SSLContext.getInstance("SSL");
ctx.init(null,mytm, null );
SSLSocketFactory sf = ctx.getSocketFactory();
JSSE 調試工具
Sun 的 JSSE 實現提供了動態調試跟蹤支持,使用系統屬性 javax.net.debug 即可。JSSE 并不正式支持這個特性,但它可以讓你看到在 SSL 通信過程中幕后在干什么。這個工具可以通過如下命令使用:
Prompt> java -Djavax.net.debug=option[debugSpecifiers] MySSLApp
如果你使用了 help 參數,它就會顯示調試選項列表。J2SE 1.4.1 中,選項如下:
|
你必須指定參數 ssl 或者 all 中的一個,緊跟 debug 符號??梢允褂靡粋€或多個調試說明符,使用“:”或者“,”作為分隔符。說明符不是必須的,但可以增強可讀性。這里是一些例子:
Prompt> java -Djavax.net.debug=all MyApp
Prompt> java -Djavax.net.debug=ssl MyApp
Prompt> java -Djavax.net.debug=ssl:handshake:trustmanager MyApp
總結
這篇文章展示了如何使用 JSSE (SSL 協議的框架和實現) 開發安全的客戶端應用程序。這篇文章中的例子展示了將 SSL 整合到 C/S 應用程序是多么簡單的事情。這篇文章中講到一個網頁瀏覽器,QBrowser,可以處理 HTTP 和 HTTPS 請求。
QBrowser 中,如果服務器上,按輸入 HTTPS 的地址中不存在有效的證書,則會拋出一個異常。你也許想修改 QBrowser 使其能夠處理這個異常并且彈出一個窗口詢問用戶是否愿意下載安裝證書,那么你可以把它做為一個練習。1.4.x 的 Java 插件使用了 JSSE,它有自己的的信任管理器,如果它不能在信任庫里找到證書,而彈出窗口提示。
原文:Secure Internet Programming with Java 2, Standard Edition (J2SE) 1.4 (Part II: The Client Side)
參閱:Secure Internet Programming with Java 2, Standard Edition (J2SE) 1.4 (Part I: The Server Side)
原文轉自:http://www.anti-gravitydesign.com