天天看點

用J2SE 1.4進行Internet安全編制程式

http://www.pcvz.com/Program/Programs/Java/JavaJC/Program_99729.html

任何在計算機網絡或者 Internet 中傳輸的消息都可能被攔截,其中不乏一些比較敏感的内容,如信用卡号或者其它一些私人資料。為了更好的在企業環境和電子商務中使用 Internet,應用軟體必須使用加 密、驗證和安全的通信協定來保護使用者的資料安全。安全超文本傳輸協定 (secure Hypertext Transfer Protocol, HTTPS) 是建立于安全套接層 (Secure Sockets Layer, SSL) 上的 HTTP,它已經成功的應用于電子商務。

Java 安全套接擴充 (Java Secure Socket Extension, JSSE) 使 Internet 安全通信成為現實。它是 100% 純 Java 實作的 SSL 架構。這個包讓 Java 開發人員能夠開發安全的網絡應用;為基于 TCP/IP 的何應用協定,如 HTTP、FTP、Telnet、或者 NTTP,在用戶端和伺服器端之間建立安全的資料通道。

JSSE 已經整合在 Java 2 SDK 标準版本 1.4 (J2SE 1.4) 中了,這真是一個好消息。這意味着隻要你安裝了 J2SE 1.4,不需要再下載下傳其它的包,就可以建立基于 SSL 的 Internet 應用程式了。這個系列的文章共有 2 篇,它是一本關于為今後的市場開發安全 Interent 應用的手冊。這篇文章主要是講的伺服器端,而下一篇是講用戶端的。這篇文章從概覽 SSL 開始,然後告訴你如何進行下列内容:

  • 使用 JSSE 的 API
  • 在你的 C/S 應用程式中結合 JSSE
  • 開發一個簡單的 HTTP 伺服器
  • 讓 HTTP 伺服器能夠處理 HTTPS 請求
  • 使用包含在 J2SE 中的 keytool 産生自己的證書
  • 開發、配置和運作一個安全的 HTTP 伺服器

概覽 SSL

SSL 協定是 Netscape 在 1994 年開發出來的,以允許服務端 (典型的如浏覽器) 和 HTTP 伺服器之間能通過安全的連接配接來通信。它加密、來源驗證、資料完整性等支援,以保護在不安全的公衆網絡上交換的資料。SSL 有這樣一些版本:SSL 2.0 有安全隐患,現在已經幾本上不用了;SSL 3.0 應用則比較廣泛;最後,由 SSL 3.0 改進而來的傳輸層加密 (Transport Layer Security, TLS) 已經成為 Internet 标準并應用于幾乎所有新近的軟體中。

在資料傳播之前,加密技術通過将資料轉變成看起來毫無意義的内容來保護資料不被非法使用。其過程是:資料在一端 (用戶端或者伺服器端) 被加密,傳輸,再在另一端解密。

來源認證是驗證資料發送者身份的一種辦法。浏覽器或者其它用戶端第一次嘗試與網頁伺服器進行安全連接配接之上的通信時,伺服器會将一套信任資訊以證書的形式呈現出來。

證書由權威認證機構 (CA)——值得信賴的授權者發行和驗證。一個證書描述一個人的公鑰。一個簽名的文檔會作出如下保證:我證明文檔中的這個公鑰屬于在該文檔中命名的實體。簽名(權威認證機構)。目前知名的權威認證機構有 Verisign,Entrust 和 Thawte 等。注意現在使用的 SSL/TLS 證書是 X.509 證書。

資料完整性就是要確定資料在傳輸過程中沒有被改變。

SSL 和 TCP/IP 協定的層次

SSL 是名符其實的安全套接層。它的連接配接動作和 TCP 的連接配接類似,是以,你可以想象 SSL 連接配接就是安全的 TCP 連接配接,因為在協定層次圖中 SSL 的位置正好在 TCP 之上而在應用層之下,如圖 1 所示。注意到這點很重要。但是,SSL 不支援某些 TCP 的特性,比如頻帶外資料。

用J2SE 1.4進行Internet安全編制程式

圖 1: SSL 和 TCP/IP 協定的的層次

可交流的加密技術

SSL 的特性之一是為電子商務的事務提供可交流的加密技術和驗證算法提供标準的方法。SSL 的開發者認識到不是所有人都會使用同一個用戶端軟體,進而不是所有用戶端都會包括任何詳細的加密算法。對于伺服器也是同樣。位于連接配接兩端的的用戶端和伺服器在初始化“握手”的時候需要交流加密和解密算法(密碼組)。如果它們沒有足夠的公用算法,連接配接嘗試将會失敗。

注意當 SSL 允許用戶端和伺服器端互相驗證的時候,典型的作法是隻有伺服器端在 SSL 層上進行驗證。用戶端通常在應用層,通過 SSL 保護通道傳送的密碼來進行驗證。這個模式常用于銀行、股份交易和其它的安全網絡應用中。

SSL 完全“握手”協定如圖 2 所示。它展示了在 SSL “握手”過程中的資訊交換順序。

用J2SE 1.4進行Internet安全編制程式

圖 2:SSL “握手”協定

這些消息的意思如下:

1. ClientHello:發送資訊到伺服器的用戶端,這些資訊如 SSL 協定版本、會話 ID 和密碼組資訊,如加密算法和能支援的密匙的大小。

2. ServerHello:選擇最好密碼組的伺服器并發送這個消息給用戶端。密碼組包括用戶端和伺服器支援。

3. Certificate:伺服器将包含其公鑰的證書發送給用戶端。這個消息是可選的,在伺服器請求驗證的時候會需要它。換句話說,證書用于向用戶端确認伺服器的身分。

4. Certificate Request: 這個消息僅在伺服器請求用戶端驗證它自身的時候發送。多數電子商務應用不需要用戶端對自身進行。

5. Server Key Exchange:如果證書包含了伺服器的公鑰不足以進行密匙交換,則發送該消息。

6. ServerHelloDone:這個消息通知用戶端,伺服器已經完成了交流過程的初始化。

7. Certificate:僅當伺服器請求用戶端對自己進行驗證的時候發送。

8. Client Key Exchage:用戶端産生一個密匙與伺服器共享。如果使用 Rivest-Shamir-Adelman (RSA) 加密算法,用戶端将使用伺服器的公鑰将密匙加密之後再發送給伺服器。伺服器使用自己的私鑰或者密鑰對消息進行解密以得到共享的密匙。現在,用戶端和伺服器共享着一個已經安全分發的密匙。

9. Certificate Verify:如果伺服器請求驗證用戶端,這個消息允許伺服器完成驗證過程。

10. Change Cipher Spec:用戶端要求伺服器使用加密模式。

11. Finished:用戶端告訴伺服器它已經準備好安全通信了。

12. Change Cipher Spec:伺服器要求用戶端使用加密模式。

13. Finished:伺服器告訴用戶端它已經準備好安全通信了。這是 SSL “握手”結果的标志。

14. Encrypted Data:用戶端和伺服器現在可以開發在安全通信通道上進行加密資訊的交流了。

JSSE

Java 安全套接擴充 (JSSE) 提供一個架構及一個 100% 純 Java 實作的 SSL 和 TLS 協定。它提供了資料加密、伺服器驗證、消息完成性和可選的用戶端驗證等機制。JSSE 的引人之外就是将複雜的、根本的加密算法抽象化了,這樣就降低了受到敏感或者危險的安全性攻擊的風險。另外,由于它能将 SSL 無縫地結合在應用當然,使安全應用的開發變得非常簡單。JSSE 架構可以支撐許多不同的安全通信協定,如 SSL 2.0 和 3.0 以及 TLS 1.0,但是 J2SE v1.4.1 隻實作了 SSL 3.0 和 TLS 1.0。

JSSE 程式設計

JSSE API 提供了擴充的網絡套接字類、信用和密匙管理,以及為簡化套接字建立而設計的套接字工廠架構,以此擴充 java.security 和 java.net 兩個包。這些類都包含在 javax.net 和 javax.net.ssl 包中。

SSLSocket 和 SSLServerSocket

javax.net.ssl.SSLSocket 是 java.net.Socket 的子類,是以他支援所有标準 Socket 的方法,和一些為安全套接字新增加的方法。javax.net.ssl.SSLServerSocket 類與 SSLSocket 類相似,隻是它用于建立伺服器套接子,而 SSLSocket 不是。

建立一個 SSLSocket 執行個體有如何兩種方法:

1. 用 SSLSocketFactory 執行個體執行 createSocket 方法來建立。

2. 通過 SSLServerSocket 的 accept 方法獲得。

SSLSocketFactory 和 SSLServerSocketFactory

javax.net.ssl.SSLSocketFactory 類是用于建立安全套接字的對象工廠。javax.net.ssl.SSLServerSocketFactory 也是這樣的工廠,但它用于建立安全的伺服器套接字。

可以通過如下方法獲得 SSLSocketFactory 執行個體:

1. 執行 SSLSocketFactory.getDefault 方法獲得一個預設的工廠。

2. 通過特定的配置行為構造一個新的工廠。

注意預設的工廠的配置隻允許伺服器驗證。

使現有的 Client/Server 應用變得安全

在現有的 C/S 應用中整合 SSL 以使其變得安全比較簡單,使用幾行 JSSE 代碼就可以做到。為了使伺服器變得安全,下面的例子中加黑顯示的内容是必須的:

import java.io.*;

import javax.net.ssl.*;



public class Server {



int port = portNumber;



SSLServerSocket server;



try {

SSLServerSocketFactory factory = 



(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();



server = (SSLServerSocket) 



factory.createServerSocket(portNumber);

SSLSocket client = (SSLSocket) 



server.accept();



// Create input and output streams as usual

// send secure messages to client through the 



// output stream



// receive secure messages from client through 



// the input stream



} catch(Exception e) {

}



}
      

為了使用戶端變得安全,下面的例子中加黑顯示的内容是必須的:

import java.io.*;

import javax.net.ssl.*;



public class Client {



...



try {



SSLSocketFactory factory = (SSLSocketFactory)



SSLSocketFactory.getDefault();



server = (SSLServerSocket) 



factory.createServerSocket(portNumber);



SSLSocket client = (SSLSOcket) 



factory.createSocket(serverHost, port);



// Create input and output streams as usual



// send secure messages to server through the 



// output stream receive secure



// messages from server through the input stream

} catch(Exception e) {



}



}
      

SunJSSE 提供者

J2SE v1.4.1 和一個 JSSE 提供者,SunJSSE 一起釋出。SunJSSE 安裝并預登記了 Java 的加密體系。請把 SunJSSE 作為一個實作的名字來考慮,它提供了 SSL v3.0 和 TLS v1.0 的實作,也提供了普通的 SSL 和 TLS 密碼組。如果你想找到你的實作 (這裡是 SunJSSE) 所支援的密碼組清單,可以調用 SSLSocket 的 getSupportedCipherSuites 方法。然而,不是所有這些密碼組都是可用的。為了找出那些是可用的,調用 getEnabledCipherSuites 方法。這個清單可以用 setEnabledCipherSuites 方法來更改。

一個完整的例子

我發現使用 JSSE 開發最複雜的事情關系到系統設定以及管理證書和密匙。在這個例子中,我示範了如何開發、配置和運作一個完整的支援 GET 請求方法的 HTTP 伺服器應用。

HTTP 概覽

超文本傳輸協定 (Hypertext Transfer Protocol, HTTP) 是一個“請求-回應”的應用協定。這個協定支援一套固定的方法如 GET、POST、PUT、DELETE 等。一般用 GET 方法向伺服器請求資源。這裡有兩個 GET 請求的例子:

GET / HTTP/1.0

GET /names.html HTTP/1.0

不安全的 HTTP 伺服器

為了開發一個 HTTP 伺服器,你得先搞明白 HTTP 協定是如何工作的。這個伺服器是一個隻支援 GET 請求方法的簡單伺服器。代碼示例 1 是這個例子的實作。這是一個多線程的 HTTP 伺服器,ProcessConnection 類用于執行不同線程中新的請求。當伺服器收到一個來自浏覽器的請求時,它解析這個請求并找出需要的文檔。如果被請求的文檔在伺服器上可用,那麼被請求的文檔會由 shipDocument 方法送到伺服器。如果被請求的文檔沒有打開,那麼送到伺服器的就是出錯消息。

代碼示例 1:HttpServer.java

import java.io.*;



import java.net.*;



import java.util.StringTokenizer;



/** 



* This class implements a multithreaded simple HTTP 



* server that supports the GET request method.



* It listens on port 44, waits client requests, and 

* serves documents.



*/



public class HttpServer {



// The port number which the server 



// will be listening on

 

public static final int HTTP_PORT = 8080;



public ServerSocket getServer() throws Exception {



return new ServerSocket(HTTP_PORT);



}



// multi-threading -- create a new connection 



// for each request



public void run() {



ServerSocket listen;



try {

listen = getServer();



while(true) {



Socket client = listen.accept();



ProcessConnection cc = new 

ProcessConnection(client);

}



} catch(Exception e) {

System.out.println("Exception: 



"+e.getMessage());



}



}



// main program

public static void main(String argv[]) throws 

Exception {

HttpServer httpserver = new HttpServer();

httpserver.run();

}

}



class ProcessConnection extends Thread {



Socket client;



BufferedReader is;



DataOutputStream os;



public ProcessConnection(Socket s) { // constructor client = s;



try {



is = new BufferedReader(new InputStreamReader



(client.getInputStream()));



os = new DataOutputStream(client.getOutputStream());

} catch (IOException e) {

System.out.println("Exception: "+e.getMessage());



}this.start(); // Thread starts here...this start() 



will call run()



}



public void run() {

try {



// get a request and parse it.



String request = is.readLine();

System.out.println( "Request: "+request );



StringTokenizer st = new StringTokenizer( request );



if ( (st.countTokens() >= 2) && 

st.nextToken().equals("GET") ) {

if ( (request = 

st.nextToken()).startsWith("/") )

request = request.substring( 1 );



if ( request.equals("") )



request = request + "index.html";



File f = new File(request);



shipDocument(os, f);



} else {



os.writeBytes( "400 Bad Request" );



} 

client.close();



} catch (Exception e) {



System.out.println("Exception: " + 



e.getMessage());



} 



}



/**



* Read the requested file and ships it 



* to the browser if found.



*/



public static void shipDocument(DataOutputStream out, 



File f) throws Exception {

try {

DataInputStream in = new 



DataInputStream(new FileInputStream(f));

int len = (int) f.length();



byte[] buf = new byte[len];

in.readFully(buf);

in.close();

out.writeBytes("HTTP/1.0 200 OK/r/n");

out.writeBytes("Content-Length: " + f.length() +"/r/n");



out.writeBytes("Content-Type: text/html/r/n/r/n");



out.write(buf);



out.flush();



} 

catch (Exception e) {out.writeBytes("/r/n/r/n");

out.writeBytes("HTTP/1.0 400 " + e.getMessage() + "/r/n");

out.writeBytes("Content-Type: text/html/r/n/r/n");



out.writeBytes("");



out.flush();



} finally {



out.close();



}



}



}
      

  實驗一下 HttpServer 類:

1. 将 HttpServer 的代碼儲存在檔案 HttpServer.java 中,并選擇一個目錄把它存放在那裡。

2. 使用 javac 編譯 HttpServer.java

3. 建立一些 HTML 檔案作為例子,要有一個“index.html”,因為它是這個例子中預設的 HTML 文檔。

4. 運作 HttpServer。伺服器運作時使用 8080 端口。

5. 打開網頁浏覽器,并送出請求:http://localhost:8080 或者 http://127.0.0.1:8080/index.html。

注意:你能想到 HttpServer 可能接收到一些惡意的 URL 嗎?比如像 http://serverDomainName:8080/../../etc/passwd 或者 http://serverDomainName:8080//somefile.txt 等。作為一個練習,修改 HttpServer 以使其不允許這些 URL 的通路。提示:寫你自己的 SecurityManager 或者使用 java.lang.SecurityManager。你可以在 main 方法的第一行添加語句 System.setSecurityManager(new Java.lang.SecurityManager) 來安裝這個安全的管理器。試試吧!

擴充 HttpServer 使其能夠處理 https://URL

現在,我要們修改 HttpServer 類,使它變得安全。我希望 HTTP 伺服器能處理 https://URL 請求。我在前面就提到過,JSSE 讓你可以很容易的把 SSL 整合到應用中去。

建立一個伺服器證書

就像我前面提到的那樣,SSL 使用證書來進行驗證。對于需要使用 SSL 來保證通信安全的用戶端和伺服器,都必須建立證書。JSSE 使用的證書要用與 J2SE 一起釋出的 Java keytool 來建立。用下列指令來為 HTTP 伺服器建立一個 RSA 證書。

prompt> keytool -genkey -keystore serverkeys -keyalg rsa -alias qusay

這個指令會産生一個由别名 qusay 引用的證書,并将其儲存在一個名為 serverkeys 的檔案中。産生證書的時候,這個工具會提示我們一些資訊,如下面的資訊,其中加黑的内容是我寫的。

Enter keystore password: hellothere

What is your first and last name?

[Unknown]: ultra.domain.com

What is the name of your organizational unit?

[Unknown]: Training and Consulting

What is the name of your organization?

[Unknown]: javacourses.com

What is the name of your City or Locality?

[Unknown]: Toronto

What is the name of your State or Province?

[Unknown]: Ontario

What is the two-letter country code for this unit?

[Unknown]: CA

Is CN=ultra, OU=Training and Consulting,

O=javacourses.com, L=Toronto, ST=Ontario, C=CA correct?

[no]: yes

Enter key password for

(RETURN if same as keystore password): hiagain

正如你所看到的,keytool 提示為 keystore 輸入密碼,那是因為讓伺服器能通路 keystore 就必須讓它知道密碼。那工具也要求為别名輸入一個密碼。如果你願意,這些密碼資訊能由 keytool 從指令行指定,使用參數 -storepass 和 -keypass 就行了。注意我使用了“ultra.domain.com”作為姓名,這個名字是為我的機器假想的一個名字。你應該輸入伺服器的主機名或者 IP 位址。

在你運作 keytool 指令的時候,它可能會花幾秒鐘的時間來産生你的密碼,具體速度得看你機器的速度了。

既然我為伺服器建立了證書,現在可以修改 HttpServer 使其變得安全了。如果你檢查 HttpServer 類,你會注意到 getServer 方法用來傳回一個伺服器套接子。也就是說,隻需要修改 getServer 方法讓它傳回一個安全的伺服器套接字就可以了。在代碼示例 2 中加黑的部分就是所做的改變。請注意我将端口号改成了 443,這是 https 預設的端口号。還有一點非常值得注意:0 到 1023 之間的端口号都是保留的。如果你在不同的端口運作 HttpsServer,那麼 URL 應該是:https://localhost:portnumber。但如果你在 443 端口運作 HttpsServer,那麼 URL 應該是:https://localhost。

示例代碼 2:HttpsServer.java

import java.io.*;



import java.net.*;



import javax.net.*;



import javax.net.ssl.*;



import java.security.*;



import java.util.StringTokenizer;



/** 



* This class implements a multithreaded simple HTTPS 



* server that supports the GET request method.



* It listens on port 44, waits client requests

* and serves documents.



*/



public class HttpsServer {

String keystore = "serverkeys";

char keystorepass[] = "hellothere".toCharArray();



char keypassword[] = "hiagain".toCharArray();



// The port number which the server will be listening on



public static final int HTTPS_PORT = 443;



public ServerSocket getServer() throws Exception {



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



ks.load(new FileInputStream(keystore), keystorepass);



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



kmf.init(ks, keypassword);



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



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



ServerSocketFactory ssf = sslcontext.getServerSocketFactory();



SSLServerSocket serversocket = (SSLServerSocket) 



ssf.createServerSocket(HTTPS_PORT);



return serversocket;



}



// multi-threading -- create a new connection 

// for each request



public void run() {

ServerSocket listen;

try {

listen = getServer();



while(true) {

Socket client = listen.accept();

ProcessConnection cc = new ProcessConnection(client);



}

} catch(Exception e) {



System.out.println("Exception: "+e.getMessage());



}

}

// main program

 

public static void main(String argv[]) throws Exception {



HttpsServer https = new HttpsServer();



https.run();



}



}
      

這幾行:String keystore = "serverkeys";

char keystorepass[] = "hellothere".toCharArray();

char keypassword[] = "hiagain".toCharArray();

指定了 keystore 的名字、密碼和密匙密碼。直接在代碼中寫出密碼文本是個糟糕的主意,不過我們可以在運作伺服器的時候在指令行指定密碼。

getServer 方法中的其它 JSSE 代碼:

  • 它通路 serverkeys keystore,JSK 是 Java KeyStore (一種由 keytool 産生的 keystore)。
  • 用 KeyManagerFactory 為 keystore 建立 X.509 密匙管理。
  • SSLContext 是實作 JSSE 的環境。用它來建立可以建立 SSLServerSocket 的 ServerSocketFactory。雖然我們指定使用 SSL 3.0,但是傳回來的實作常常支援其它協定版本,如 TLS 1.0。舊的浏覽器中更多時候使用 SSL 3.0。

注意預設情況下不需要用戶端的驗證。如果你想要伺服器請求用戶端進行驗證,使用:

serversocket.setNeedClientAuth(true).

現在用 HttpsServer 類做個實驗:

1. 将 HttpsServer 和 ProcessConnection 兩個類 (上面的代碼) 儲存在檔案 HttpsServer.java 中。

2. 讓HttpsServer.java 與 keytool 建立的 serverkyes 檔案在同一目錄。

3. 使用 javac 編譯 HttpsServer。

4. 運作 HttpsServer。預設情況下它應該使用 443 端口,不過如果你不能在這個端口上使用它,請選擇另一個大于 1024 的端口号。

5. 打開網頁浏覽器并輸入請求:https://localhost 或者 https://127.0.0.1。這是假譯伺服器使用 443 端口的情況。如果不是這個端口,那麼使用:use: https://localhost:port

你在浏覽器中輸入 https://URL 的時候,你會得到一個安全警告的彈出視窗,就像圖 3 那樣。這是因為 HTTP 伺服器證書是自己産生的。換句話說,它由未知的 CA 建立,在你的浏覽器儲存的 CA 中沒有找到這個 CA。有一個選項讓你顯示證書 (檢查它是不是正确的證書以及是誰簽的名) 和安裝該證書、拒絕該證書或者接受該證書。

用J2SE 1.4進行Internet安全編制程式

圖 3:由未知 CA 頒發的伺服器證書

注意:在内部的私有系統中産生你自己的證書是個很好的主意。但在公共系統中,最好從知名的 CA 處獲得證書,以避免浏覽器的安全警告。

如果你接受證書,你就可以看到安全連接配接之後的頁面。以後通路同一個網站的時候浏覽器就不再會彈出安全警告了。注意有許多網站使用 HTTPS,而證書是自己産生或者由不知名的 CA 産生的。例如,https://www.jam.ca。如果你沒通路過這個網頁,你會看到一個像圖 3 一樣的安全警告。

注意:你接受證書以後,它隻對目前的會話有效,也就是說,如果你完全退出浏覽器後,它就失效了。Netscape 和 Microsoft Internet Explorer (MSIE) 都允許你永久保證證書。在 MSIE 中的作法是:選擇圖 3 所示的“View Certificate”并在新開的視窗中選擇“Install Certificate”。

總結

這篇文章談到了 SSL 并描述了 JSSE 架構及其實作。文中的例子可以說明把 SSL 整合到你的 C/S 應用中是一件很容易的事情。文中給出了一個安全 HTTP 伺服器的例子,你可以使用它來進行實驗。文中還介紹了 JSSE API 以及可以發生 HTTPS 請求的網頁浏覽器。

繼續閱讀