H5W3
当前位置:H5W3 > 其他技术问题 > 正文

用J2SE1.4进行Internet安全编程

服务器端

任何在计算机网络或者 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 的特性,比如频带外数据。

图 1: SSL 和 TCP/IP 协议的的层次

可交流的加密技术

SSL 的特性之一是为电子商务的事务提供可交流的加密技术和验证算法提供标准的方法。SSL 的开发者认识到不是所有人都会使用同一个客户端软件,从而不是所有客户端都会包括任何详细的加密算法。对于服务器也是同样。位于连接两端的的客户端和服务器在初始化“握手”的时候需要交流加密和解密算法(密码组)。如果它们没有足够的公用算法,连接尝试将会失败。

注意当 SSL 允许客户端和服务器端相互验证的时候,典型的作法是只有服务器端在 SSL 层上进行验证。客户端通常在应用层,通过 SSL 保护通道传送的密码来进行验证。这个模式常用于银行、股份交易和其它的安全网络应用中。

SSL 完全“握手”协议如图 2 所示。它展示了在 SSL “握手”过程中的信息交换顺序。

图 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。有一个选项让你显示证书 (检查它是不是正确的证书以及是谁签的名) 和安装该证书、拒绝该证书或者接受该证书。

图 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 请求的网页浏览器。

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 的网页浏览器

我们开发一个支持 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);

content.setContentType("text/html");

content.setText("

Q's Browser

 

 

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 + "

" + trace + "

");

 

}

/**

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

}

}

");

既然 QBrowser 使用 URL 类,它就可以处理 HTTP 和 HTTPS 请求。你可以使用 HTTP 和 HTTPS 地址测试 QBrowser。这里是一些测试:

 

1、请求 http://www.javacourses.com,你会看到如图 1 所示的内容。

图 1:http://www.javacourses.com

2、请求 https://www.jam.ca,结果抛出了异常。因为这个网页服务器的证书不受信任并且不能在默认页中找到,所以它抛出如图 2 所示的异常。

图 2:https://www.jam.ca

3、请求 https://localhost,这里运行着第一部分中写的 HttpServer。注意,如果你使用命令 java QBrowser 来运行 QBrowser,而服务器的证书导出后被导入默认文件 jssecacerts,那么应该将该文件拷贝到 java.home 目录的 lib/security 子目录中。如果证书被导入了其它文件,你可以使用 trustStore 选项,如:java -Djavax.net.ssl.trustStore=file QBrowser。使用其实任何一种方法,浏览器都会工作,并且你可以看到如图 3 所示的默认页面。

图 3:https://localhost

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

}

}

现在试试 ReadHttpsURL3,完成上面讨论的内容。注意,无论如何,既然我们使用 URL 类,你就能在命令行指定 URL,包括协议的名称。这里是一个例子:

Prompt> java ReadHttpsURL3 https://www.sun.com

HttpsURLConnection 有一个非常有趣的特点:一旦获得了连接,你就可以在网络连接之前使用一些有用的参数对其进行配置,如 HostnameVerifier。HostnameVerifier 是一个接口,它申明了方法:public boolean verify (String hostname, SSLSession session)。而且,它像下面所述的那样工作:

如果 SSL/TLS 标准主机名校验逻辑失败,执行过程中会调用回调类的 verify 方法。回调类是实现了 HostnameVerifier 接口的类。

如果回调类检查到主机名可以接受,则允许连接,否则,连接会被终止。

回调类遵循的规则即可以是基本的验证方法,也可以依赖其它验证方法。这里说明了如何实现:

public class MyVerified implements HostnameVerifier {

public boolean verify(String hostname, SSLSession session) {

// pop up a dialog box

// ...

// return either true or false

}

}

现在,可以这样使用它:

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[] getAcceptedIssuers() {

}

}

为了支持远端套接字 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 中,选项如下:

all turn on all debugging

ssl turn on ssl debugging

The following can be used with ssl:

record enable per-record tracing

handshake print each handshake message

keygen print key generation data

session print session activity

defaultctx print default SSL initialization

sslctx print SSLContext tracing

sessioncache print session cache tracing

keymanager print key manager tracing

trustmanager print trust manager tracing

handshake debugging can be widened with:

data hex dump of each handshake message

verbose verbose handshake message printing

record debugging can be widened with:

plaintext hex dump of record plaintext

你必须指定参数 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)

本文地址:H5W3 » 用J2SE1.4进行Internet安全编程

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址