J2SE 1.4网络安全编程
作者:Qusay H. Mahmoud
2002年11月
译者:FlyBean
任何在计算机网络或者互联网上传输的数据都可能被截听。其中一些信息可能是敏感的,比如信用卡号和其它私人数据。为了在企业环境以及电子商务应用中更好地使用互联网,应用必须使用加密、认证和安全通讯协议来保护用户数据。安全超文本传输协议,一个基于安全套接口层的 HTTP 协议,早已成功地应用于电子商务应用。
JAVA安全套接口扩展(JSSE)是一套提供安全互联网通信功能的JAVA包,是用100%纯JAVA实现的安全套接口层(SSL)的框架。这些包允许作为JAVA开发人员的您开发安全的网络应用,运行任何基于TCP/IP的应用协议,如:HTTP、FTP、Telnet或NNTP等,在客户机与服务器之间建立安全的数据通道。
一个好消息是Java 2 SDK标准版1.4版本(J2SE 1.4)已经集成了JSSE。这意味着如果您已经安装了J2SE 1.4,那么您无需下载任何额外的包,就可以创建基于SSL的安全的互联网应用。本系列文章共分为两个部份,它提供了一个易于实践的教程,说明了如何为今日和未来的市场开发安全的互联网应用。本文所关注的是服务器,下一篇文章关注的是客户机。本文将以介绍SSL的详细概述开始,向你展示以下内容:
- 使用 JSSE API;
- 将 SSL 集成进您已有的客户机 - 服务器应用;
- 开发简单的 HTTP 服务器;
- 修改 HTTP 服务器,使之能处理 HTTPS 请求;
- 使用 J2SE 中的 keytool 程序产生您自己的证书;
- 开发、配置和运行一个安全的 HTTP 服务器。
SSL概述
SSL协议是由Netscape于1994年开发的,它允许客户机(通常是Web浏览器)和HTTP服务器通过安全的连接进行通信。作为在不安全的公网上所交换信息的保护手段,它提供加密、来源认证和数据完整性。当前存在多个SSL版本:SSL 2.0存在安全缺陷,现在已经极少使用;SSL 3.0得到了广泛的支持;最后,作为SSL 3.0的改进版本,传输层安全(TLS)被采纳为互联网标准,并被几乎所有新近的软件所支持。
加密保护数据不受未经授权的访问,这是通过在传输前将数据转换为表面上没有任何意义的数据形式来实现的。数据在一端被加密(客户机或服务器)、传送并由另一端解密,然后处理。
来源认证是检验数据发送者身份的一种方法。当浏览器或其它客户机第一次试图通过安全连接进行通信时,服务器以证书的形式发送客户机一套凭证。
证书是由可信赖的机构,称之为认证中心(CA)发布和验证的。证书代表着一个人的公开身份。它就象一个签名文件:我证明在文件中的公钥属于在该文件中命名的实体。签名:(认证中心)。知名的CA包括Verisign、Entrust和Thawte。请注意:当前SSL/TLS使用的证书是X.509证书。
数据完整性是指确保在传输过程中数据不被修改。
SSL和TCP/TP协议栈
正如SSL的名称-安全套接口层所指出的,SSL连接与TCP的套接口连接类似。由于在协议栈中SSL连接恰恰位于TCP连接之上和应用层之下(如图1所示),因此,您可以认为SSL连接是安全的TCP连接。然而,需要注意的是,SSL并不支持诸如带外数据(out-of-bound)的TCP特性。
协商式加密
正是由于SSL的特性使得它成为安全的电子商务事务处理事实上的标准传输手段。SSL对协商式加密以及认证算法的支持就是其中的两个特性。SSL的设计者意识到并非所有的参与者都使用相同的客户端软件,因此并非所有的客户机将包括特定的加密算法。对服务器而言,也是如此。客户机与服务器是一个连接的两端。在它们最初的握手时,它们将协商所使用的加密/解密算法(加密套件)。如果双方没有共同的满足需要的算法,那么在这种情况下,连接的尝试将失败。
需要注意的是,尽管SSL允许客户机与服务器双方相互认证,但通常只有服务器是在SSL层中认证的。而客户机则通常是通过使用SSL保护的通道传递口令的方式,在应用层中认证的。这种模式在银行、证券交易以及其它安全的WEB应用中普遍使用。
图2图解说明了SSL完整的握手协议。它展示了在SSL握手过程中消息交换的顺序。
消息的含义:
1. ClientHello:客户机向服务器发送信息,诸如SSL协议版本、会话ID以及加密套件信息,如加密算法和所支持的密钥长度;
2. ServerHello:服务器选择客户机和服务器都支持的最佳的加密套件,并将信息发送给客户机;
3. Certificate:服务器向客户机发送包含了服务器公钥的证书。此消息是可选的,在要求服务器认证时需要使用。换句话说,它被用来向客户机确认服务器的身份;
4. Certificate Request:仅当服务器要求客户机认证自身时,此消息才被发送。大多数电子商务应用并不要求客户机对自己进行认证。
5. Server Key Exchange:如果包含了服务器公钥的证书不能满足密钥交换的需要,此消息被发送;
6. ServerHelloDone:此消息告知客户机,服务器已经完成了最初的协商过程;
7. Certificate:仅当服务器要求客户机认证自身时,此消息才被发送。
8. Client Key Exchange:客户机为客户机和服务器产生一个共享的秘密密钥(Secret key)。如果使用了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引人注目的一点是: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实例:
1. 通过调用SSLSocketFactory类的实例的某个createSocket方法;
2. 通过SSLServerSocket的accept方法。
SSLSocketFactory和SSLServerSocketFactory
javax.net.ssl.SSLSocketFactory类是创建安全套接口的对象工厂,javax.net.ssl.SSLServerSocketFactory 是创建服务端套接口的对象工厂。
可以通过以下两种方法得到SSLSocketFactory实例:
1. 调用SSLSocketFactory.getDefault()方法获取默认的工厂;
2. 使用指定配置行为,构建新的工厂。
注意:默认的工厂被配置为仅允许服务器认证。
增加已有的客户机/服务器应用的安全性
将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。安装了J2SE v1.4.1,也就安装了SunJSSE,并已使用Java加密体系结构(JCA)进行了注册。我们可以认为SunJSSE提供者就是该实现的名字。它不仅提供了SSL v3.0和TLS v1.0r 实现,也提供了最通用的SSL和TLS加密套件。您可以调用SSLSocket类的getSupportedCipherSuites()方法,来获取您所采用的JSSE实现(此处为SunJSSE)的加密套件清单。然而,并非所有的这些加密套件都是被允许的。您可以调用getEnabledCipherSuites方法获取哪些加密套件是被允许的。允许的加密套件清单可以通过调用setEnabledCipherSuites方法修改。
完整的例子
我发现使用JSSE时最复杂的问题是与系统配置以及证书和密钥的管理相关的。通过本例,我将示范如何开发、配置并运行一个完整的、支持GET请求方法的HTTP服务器应用。
HTTP概述
超文本传输协议(HTTP)是请求-响应应用协议。该协议支持确定的方法集,如GET、POST、PUT、DELETE等等。GET方法通常用来向WEB服务器请求资源。下面是两个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文件,包括“include.html”,这是本例中默认的文档。
4. 运行HttpServer,它使用8080端口。
5. 打开WEB浏览器,访问如下地址:
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://请求
现在,让我们着手修改HttpServer类,使之安全。我喜欢HTTP服务器能处理https:// URL请求。正如我前面所提到的,JSSE允许您非常简单地将SSL集成进您的应用中。
创建服务器证书
前面我已提到过,SSL使用证书来认证。必须为需要使用SSL进行安全通信的客户机与服务器创建证书。JSSE使用随J2SE发布的keytool工具创建的证书。我使用如下命令为HTTP服务器创建一个RSA证书:
prompt> keytool -genkey -keystore serverkeys -keyalg rsa -alias qusay
该命令将创建一个由别名qusay引用的证书,并将存贮为serverkeys的命名文件。Keytool工具提示我产生证书的一些信息,我所键入的以黑体显示:
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 <qusay>
(RETURN if same as keystore password): hiagain
您可以看到,keytool提示我为密钥库(keystore)输入口令,这意味着如果服务器要访问密钥库,那么它必须知道该口令。同样,该工具也要求我为别名输入口令。当然,如果您愿意,您也可以使用-storepass和-keypass选项,在keytool命令行输入这些口令。请注意我使用“ultra.domain.com”做为姓和名,它是我机器的假想名字。您可以输入HTTP服务器所在机器的主机名或IP地址。
当您运行keytool命令时,它可能会花上一些时间来产生证书,这是由您的机器所决定的。
一旦为我的服务器产生了证书,我就可以修改我的HttpServer,使之安全。如果您检查过HttpServer类,您将注意到getServer方法用来得到一个服务端套接口。这意味着,我需要修改的唯一方法就是getServer方法,让它返回一个安全服务端套接口。在代码示例2中,所做的改变以黑体突出显示。注意到我将端口号改为了443,这是HTTPS的默认端口号。注意:端口号0到1023是保留的,这一点很重要!如果您以不同的端口运行HttpServer,那么URL应该是:
https://localhost:portnumber
如果您使用443端口,那么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();
指定了密钥库的名字和口令,以及密钥的口令。对于产品化代码而言,在代码中硬编码口令并不是个好主意。然而,可以在使用本服务器时通过命令行方式予以指定。
剩下的与JSSE相关的代码就是getServer方法:
l 它访问serverkeys的密钥库。JKS是Java密钥库(由keytool创建的一种密钥库)。
l KeyManagerFactory被用来创建用于密钥库的X.509密钥管理器。
l SSLContext是实现JSSE的环境。它用来创建一个ServerSocketFactory,然后使用该工厂对象创建SSLServerSocket。尽管我们指定为SSL 3.0,但是所返回的实现通常支持其它协议版本,诸如:TLS 1.0。然而,旧的浏览器广泛地使用了SSL 3.0。
请注意:在默认情况下,客户机认证不是必需的。如果您希望您的服务器要求客户机认证,使用下面的语句:
serversocket.setNeedClientAuth(true).
按以下步骤试验HttpServer类:
1. 将HttpsServer和ProcessConnection类拷贝为HttpsServer.java文件。
2. 将该文件保存在由keytool工具创建的serverkeys文件所在的目录中。
3. 使用javac编译HttpsServer.java。
4. 运行HttpsServer。默认情况下,它使用443端口。如果在此端口上无法启动服务器,选择一个大于1024的端口号。
5. 打开浏览器,键入以下请求:
https://localhost 或者 https://127.0.0.1。这是假设服务器使用的是443端口。如果不是,则使用以下格式:
https://localhost:port
当您在浏览器中输入https:// URL后,您将得到一个弹出的安全警告窗口,类似图3。这是由于HTTP服务器证书是由它自己产生的。换句话说,它由未知的认证机构产生的,您的浏览器无法在它所保存的认证机关清单中找到该认证机关。您可以查看该证书(检查是否是正确的证书以及发现是谁签名的),然后安装,拒绝或接受该证书。
注意:
对于内部的私有系统而言,创建您自己的证书是不错的主意。然而,对于公用系统,为了避免浏览器安全警告,可以从知名的认证机关(CA)获取证书。这是个很好的主意。
如果您接受了该证书,您就能访问在此安全连接之后的页面,以后对同一个WEB站点的访问将不会导致浏览器弹出警告窗口。需要注意的是:许多使用HTTPS的WEB站点所发布的证书要么是自己产生的,要么是由其它未知的CA产生的。作为一个例子,您可以访问http://www.jam.ca。如果您从未访问过此站点,您将看到如图3所示的安全警告。
请注意:
当您接受了该证书,它就仅在此次会话中有效。换句话说,一旦您完全退出浏览器,证书就“消失”了。 Netscape 和 Microsoft Internet Explorer(MSIE)都支持永久性地安装证书。在MSIE中,在图3选择“View Certificate”(查看窗口),然后在新窗口中选择“Install Certificate”(安装证书)。
结论
本文介绍了SSL的详细概述,描述了JSSE框架及实现。本文所介绍的例子展示了无缝地将SSL集成进您已有的客户机/服务器应用是如何的简单。本文也给出了一个安全的HTTP服务器,您可以从它获取一些经验。深入持续地学习,以获取有关JSSE API以及WEB浏览器处理HTTPS请求能力的更多信息。
更多信息
- SSL 3.0
- TLS 1.0
- Download J2SE 1.4.1
- Java Secure Socket Extension (JSSE)
- JSSE Reference Guide
- The J2SE 1.4.1 java.net Package
- J2SE 1.4.1 Documentation
- J2SE 1.4.1 Networking Properties
答谢
特别感谢Sun微系统公司的Andreas Sterbenz,他的建议帮助我提高本文的质量。
关于作者
Qusay H. Mahmoud 提供Java咨询及培训服务。他发表了许多关于Java的文章。他也是《分布式Java编程》(Distributed Programming with Java,Manning Publications,1999)和《学习无线Java》(Learning Wireless Java,O’Reilly, 2002)两书的作者。
- 相关文章
- 最新文章
- 用于Web服务的核心Java API[12-12]
- 数据挖掘器: 警告 — 及时采取行..[12-12]
- “总览图”: IBM DB2 通用数据库..[12-12]
- UNIX、Linux 和 Windows 的生动简..[12-12]
- DB2 大事记[12-12]
- 数据管理部门副总裁兼 CTO:Don ..[12-12]
