一个完整的例子
我发现使用 JSSE 开发最复杂的事情关系到系统设置以及管理证书和密匙。在这个例子中,我演示了如何开发、配置和运行一个完整的支持 GET 请求方法的 HTTP 服务器应用。
HTTP 概览
超文本传输协议 (Hypertext Transfer Protocol, HTTP) 是一个“请求-回应”的应用协议。这个协议支持一套固定的方法如 GET、POST、PUT、DELETE 等。一般用 GET 方法向服务器请求资源。这里有两个 GET 请求的例子:
GET / HTTP/1.0 <empty-line>
GET /names.html HTTP/1.0 <empty-line>
不安全的 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("<html><head><title>error</title>
</head><body>/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("</body></html>");
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
然后输入:keytool –genkey,这个命令会产生一个由别名 qusay 引用的证书,并将其保存在一个名为 .keystore的文件中。产生证书的时候,这个工具会提示我们一些信息,如下面的信息,其中加黑的内容是我写的。
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 = ".keystore";
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 = ".keystore";
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 请求的网页浏览器。
|