第7章 数据的安全传输和身份验证 ——SSL和HTTPS编程
本章重点:
本章在前面几章介绍的加密和认证技术的基础上,介绍如何使用SSL协议加密TCP/IP数据流,并介绍基于SSL的、用于加密浏览器和Web服务器之间通信的HTTPS协议。
SSL和HTTPS不仅可以加密通信,而且可以用于服务器和客户身份的验证。用户浏览器访问一个站点,需要确定这个站点确实是某个机构的(说不定黑客已经攻击了你使用的域名服务器,将你导向了黑客伪装的一个站点)。服务器再某些时候也可能需要确定用户是谁,以便决定是否向其提供某类信息。本章对此作了介绍。
本章主要内容:
l 编制SSL客户和服务器程序
l 编制HTTPS客户和服务器程序
l 设置服务器所使用的证书
l 设置客户程序信任的证书
l 设置客户程序所使用的证书
l 设置服务器信任的证书
7.1 最简单的SSL通信
SSL编程使用客户机/服务器模式,二者之间的通信使用SSL协议进行加密。本节先通过最简单的程序介绍服务器和客户程序之间如何通过SSL进行加密通信。
7.1.1 最简单的SSL服务器
★ 实例说明
本实例编写了一个最简单的SSL服务器程序,它接受客户程序建立连接,并以加密方式向客户程序发送一串字符Hi。
SSL服务器程序运行时需要指定密钥库,以便向客户程序证明自己的身份。本实例演示了通过编程指定密钥库和通过java命令选项指定密钥库的两种运行方式。
★ 编程思路:
SSL编程和基于Socket的编程类似,首先创建ServerSocket对象,传入端口号,然后执行ServerSocket对象的accept( )方法获取Socket类型的对象,并侦听端口以等待客户程序和服务器连接。最后通过Socket类型的对象获得输入和输出流,通过输入和输出流和客户程序进行通信。SSL编程和基于Socket的编程不同的地方在于其ServerSocket对象是通过一个特殊的对象:SSLServerSocketFactory类型的对象创建的,这样以后的输入和输出流将自动按照SSL协议指定的方法交换密钥并对数据进行加密。此外,需要指定包含证书的密钥库,以便客户程序确定SSL服务器是否可靠。
具体步骤如下:
(1) 设置密钥库及口令
System.setProperty("javax.net.ssl.keyStore",
"mykeystore");
System.setProperty("javax.net.ssl.keyStorePassword",
"wshr.ut");
分析:通过System类的静态方法setProperty( )可以设置系统参数。方法的第一个参数是系统参数的名称,第二个参数是为系统参数设置的值。作为SSL服务器程序,主要需要设置两个系统参数:javax.net.ssl.keyStore指定密钥库的名称,javax.net.ssl.keyStorePassword指定密钥库的密码。
这里不妨使用5.1节得到的密钥库mykeystore,其密码为wshr.ut。密钥库中必须存放私钥和证书,此外为私钥设置的密码应该和密钥库的密码相同。程序将自动从密钥库中提取证书。
(2) 创建SSLServerSocketFactory类型的对象
SSLServerSocketFactory ssf= (SSLServerSocketFactory)
SSLServerSocketFactory.getDefault( );
分析:执行javax.net.ssl包中SSLServerSocketFactory类的静态方法getDefault( ),经过强制转换获得SSLServerSocketFactory类型的对象,后面将用它获取ServerSocket对象。
(3) 创建ServerSocket类型的对象
ServerSocket ss=ssf.createServerSocket(5432);
分析:执行上一步得到的SSLServerSocketFactory对象的createServerSocket( )方法获得ServerSocket类型的对象,方法参数中指定一个整数作为端口号,其值一般在1~ 65535之间,其中1~1023一般用于知名的端口号或特定的UNIX服务,临时使用的端口号可取1024~ 65535之间的整数。
一台计算机上往往会运行不同的服务程序提供不同的服务,这些程序应使用不同的端口号,这样,当服务器收到客户程序发来的请求时,通过端口号确定哪个服务器程序与之通信。
(4) 等待客户程序连接
Socket s=ss.accept( );
分析:执行上一步得到的ServerSocket对象的accept( )方法,程序将在此处挂起,等待客户程序建立连接。该方法返回的Socket类型的对象可用于和客户程序之间的通信。
(5) 建立输出流
PrintStream out = new PrintStream(s.getOutputStream( ));
out.println("Hi");
分析:执行上一步得到的Socket对象的getOutputStream( )方法可以得到输出流,通过该输出流发送的信息将加密传递给客户程序。这里不妨使用输出流创建PrintStream类型的对象,以便通过println( )语句向客户程序打印字符串。
如果服务器程序同时需要处理客户程序发来的字符串,可以再通过Socket对象的getInputStream( )方法得到输入流,从输入流读取的信息即客户发来的信息。
★代码与分析:
完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MySSLServer{
public static void main(String args[ ]) throws Exception{
System.setProperty("javax.net.ssl.keyStore",
"mykeystore");
System.setProperty("javax.net.ssl.keyStorePassword",
"wshr.ut");
SSLServerSocketFactory ssf=(SSLServerSocketFactory)
SSLServerSocketFactory.getDefault( );
ServerSocket ss=ssf.createServerSocket(5432);
System.out.println("Waiting for connection...");
while(true){
Socket s=ss.accept( );
PrintStream out = new PrintStream(s.getOutputStream( ));
out.println("Hi");
out.close( );
s.close( );
}
}
}
为了让程序接受到一个连接请求并发送完“Hi”后能继续接受其他客户程序建立连接,程序中将Socket s=ss.accept( )及其输入/输出处理放在了一个while循环当中。
★运行程序
由于SSL协议需要通过数字证书向客户表明服务器是否值得信任,因此当前目录下必须有密钥库,本实例不妨使用5.1节得到的密钥库mykeystore,将其拷贝到当前目录下,然后输入“java MySSLServer”运行程序,当等待一段时间完成初始化后,屏幕显示:“Waiting for connection...” ,此时开始等待客户程序的连接。
编程第1步设置系统参数也可以不在程序中指定,而是通过java命令选项来指定。例如如果省略了编程第1步,则可输入“java -Djavax.net.ssl.keyStore=mykeystore -Djavax.net.ssl.keyStorePassword=wshr.ut MySSLServer”来运行程序,这样程序本身更具有灵活性。
为了使客户程序能够顺利验证该服务器提供的证书,应该把mykeystore中所使用的证书或其签发者的证书提供给客户程序。这在下一小节“运行程序”部分将详细说明。
7.1.2 最简单的SSL客户程序
★ 实例说明
本实例编写了一个最简单的SSL客户程序,它和运行7.1.1小节程序的计算机建立连接,接受其发来的字符串并自动对其进行解密。本实例同时演示了通过程序指定密钥库和通过java命令选项指定密钥库的两种运行方式。
★ 编程思路:
SSL客户端的编程也和基于Socket的客户端编程类似。首先得到Socket类型的对象,然后通过Socket类型的对象获得输入和输出流,通过输入和输出流和服务器程序进行通信。和服务器程序类似,SSL客户端编程和基于Socket的客户端编程不同的地方在于其Socket对象是通过一个特殊的对象:SSLSocketFactory类型的对象创建的。
具体步骤如下:
(1) 设置客户程序信任的密钥库
System.setProperty("javax.net.ssl.trustStore",
"clienttrust");
分析:客户端欲和SSL服务器通信,则必须信任SSL服务器程序所使用的数字证书。因此客户程序应该将所信任的证书放在一个密钥库中(本实例“运行程序”部分给出了如何创建这样的密钥库)。这里不妨假定客户程序信任的证书放在文件名为clienttrust的密钥库中。
通过System类的静态方法setProperty( )可以设置系统参数javax.net.ssl.trustStore,可以在程序中指定该文件名。由于clienttrust中存放的只是可以公开的证书,因此程序中不需要给出密钥库的密码。
(2) 创建SSLSocketFactory类型的对象
SSLSocketFactory ssf= (SSLSocketFactory)
SSLSocketFactory.getDefault( );
分析:执行javax.net.ssl包中SSLSocketFactory类的静态方法getDefault( ),经过强制转换获得SSLSocketFactory类型的对象,后面将用它获取Socket对象。
(3) 创建Socket类型的对象,连接服务器程序
Socket s = ssf.createSocket("127.0.0.1", 5432);
分析:执行上一步得到的SSLSocketFactory对象的createSocket( )方法和服务器指定端口建立连接。方法的第一个参数是字符串形式的服务器IP地址或域名,如果只有一台计算机,客户和服务器程序都在同一台计算机上运行,则可以使用“127.0.0.1”作为服务器的IP地址,或“Localhost”作为服务器的域名。第二个参数即7.1.1小节的服务器程序在第3步指定的端口号。
(4) 建立输出流
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
String x=in.readLine( )
分析:执行上一步得到的Socket对象的getInputStream( )方法可以得到输入流,通过该输入流读取服务器程序发送来的信息并自动解密。这里不妨使用输入流创建BufferedReader类型的对象,以便通过readLine( )语句读取字符串。
如果客户程序同时需要向服务器程序发送信息,可以再通过Socket对象的getOutputStream( )方法得到输出流,通过输出流发送的信息可以被服务器程序的输入流读取到。
★代码与分析:
完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MySSLClient{
public static void main(String args[ ]) throws Exception {
System.setProperty("javax.net.ssl.trustStore",
"clienttrust");
SSLSocketFactory ssf=
(SSLSocketFactory) SSLSocketFactory.getDefault( );
Socket s = ssf.createSocket("127.0.0.1", 5432);
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
String x=in.readLine( );
System.out.println(x);
in.close( );
}
}
★运行程序
服务器程序使用的密钥库是5.1.3小节得到的密钥库mykeystore,假定已经用5.2.3小节的方法得到了证书文件mytest.cer。将该文件存放在客户程序所在目录中,以便客户程序向服务器程序确认信任该证书。为了在程序中使用,需将该证书导入密钥库,操作如下:
C:\java\ch7\Client>keytool -import -alias mytest -file mytest.cer -keystore clienttrust
输入keystore密码: 123456
Owner: CN=Xu Yingxiao, OU=Network Center, O=Shanghai University, L=ZB, ST=Shangh
ai, C=CN
发照者: CN=Xu Yingxiao, OU=Network Center, O=Shanghai University, L=ZB, ST=Shan
ghai, C=CN
序号: 3deec043
有效期间: Thu Dec 05 10:56:03 CST 2002 至: Sun Nov 17 10:56:03 CST 2013
认证指纹:
MD5: B2:DC:75:CD:60:B7:1E:7A:97:EE:E8:A4:31:D6:26:C6
SHA1: 32:E5:89:16:7E:25:7F:86:16:94:34:36:95:44:D7:CF:14:C8:F2:1E
信任这个认证? [否]: 是
认证已添加至keystore中
该操作将证书my.cer导入密钥库clienttrust。
运行程序之前检查一下7.1.1小节的程序是否已经运行,其DOS窗口停留在“Waiting for connection...”提示语句。客户程序可以在同一台计算机上再开设一个DOS窗口来运行,也可在另一台联网的计算机上运行,这时程序中的IP地址:127.0.0.1应该改为运行7.1.1小节服务器程序所在计算机的实际IP地址。
在DOS窗口输入“java MySSLClient”运行客户程序,程序将显示服务器程序发来的“Hi”。如果用抓包软件捕捉客户程序和服务器程序之间的通信,可以发现通信内容是以密文传递的。
和服务器程序一样,编程第1步设置系统参数也可以不在程序中指定,而是通过java命令选项来指定。例如如果省略了编程第1步,则可输入“java -Djavax.net.ssl.trustStore=clienttrust MySSLClient”来运行程序,这样程序本身更具有灵活性。
7.1.3 进一步设置信任关系
★ 实例说明
7.1.1和7.1.2小节的例子中使用的密钥库mykeystore和证书mytest.cer是自签名的证书,本实例的服务器程序使用6.1.1小节得到的密钥库lfkeystore2中的证书“Liu Fang”,该证书是CA “Xu Yingxiao”签发的,而客户程序不是直接信任信任证书“Liu Fang”,而是信任CA “Xu Yingxiao”的证书。
本实例同时演示了通过java命令选项来指定密钥库及密码。
★ 编程思路:
服务器程序和7.1.1小节类似,只是密钥库使用lfkeystore2即可,密钥库的密码仍旧为wshr.ut。服务器使用该密钥库中的证书“Liu Fang”向客户程序表明自己的身份。
7.1.2小节的客户程序信任的证书为mytest.cer,即CA “Xu Yingxiao”的证书,由于lfkeystore2中的证书是CA “Xu Yingxiao”签发的,因此客户程序即使没有直接信任“Liu Fang”的证书,只要信任CA “Xu Yingxiao”的证书,则自动信任“Liu Fang”的证书。因此客户程序不需要作修改。
★代码与分析:
服务器程序完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MySSLServer2{
public static void main(String args[ ]) throws Exception{
SSLServerSocketFactory ssf=
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault( );
ServerSocket ss=ssf.createServerSocket(5432);
System.out.println("Waiting for connection...");
while(true){
Socket s=ss.accept( );
PrintStream out = new PrintStream(s.getOutputStream( ));
out.println("Hi");
out.close( );
s.close( );
}
}
}
这里为了演示通过java命令选项来指定密钥库及密码,在程序中删除了System.setProperty( )语句。
★运行程序
运行服务器程序的目录下存放6.1.1小节得到的密钥库文件lfkeystore2,输入:“java -Djavax.net.ssl.keyStore=lfkeystore2 -Djavax.net.ssl.keyStorePassword=wshr.ut MySSLServer2”运行程序,和7.1.1小节一样,显示“Waiting for connection... ”提示,等待用户连接。
和7.1.2小节一样运行客户程序,尽管该程序没有直接信任服务器所使用的证书,但服务器的证书是客户所信任的证书签发的,因此程序可以正常运行。
7.1.4 设置默认信任密钥库
★ 实例说明
7.1.3小节的服务器程序使用CA“Xu Yingxiao”签发的证书“Liu Fang”,客户程序在运行时仍需要通过System.setProperty( )方法或者Java命令选项设置客户程序信任什么证书。
本实例使用默认信任密钥库指定客户程序信任哪些证书。
★ 编程思路:
服务器程序使用7.1.3小节的程序,客户程序只要信任证书“Liu Fang”或者其签发者CA“Xu Yingxiao”的证书即可。
7.1.2小节的客户程序通过System.setProperty( )方法或者Java命令选项设置了系统参数:javax.net.ssl.trustStore,指定了客户程序信任哪些证书。
如果使用默认信任密钥库,则不需要在程序或Java命令中指定系统参数,因此只要将7.1.2小节的程序中System.setProperty( )语句去掉即可。
Java默认的信任密钥库是C:\j2sdk1.4.0\jre\lib\security目录下的cacerts文件,使用J2SDK提供的keytool工具可以将客户信任的证书导入该密钥库,则Java程序自动信任这些证书对应的CA签发的证书。
在默认信任密钥库中已经存有一些著名CA的证书,如果服务器程序所使用的证书是这些CA签发的,则不需要修改默认信任密钥库。
★代码与分析:
完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MySSLClient2{
public static void main(String args[ ]) throws Exception {
SSLSocketFactory ssf=
(SSLSocketFactory) SSLSocketFactory.getDefault( );
Socket s = ssf.createSocket("127.0.0.1", 5432);
BufferedReader in
= new BufferedReader(
new InputStreamReader(s.getInputStream( )));
String x=in.readLine( );
System.out.println(x);
in.close( );
}
}
★运行程序
由于使用默认信任密钥库,因此运行程序时不需要Java命令选项,只要输入“java MySSLClient2”运行程序即可。运行之前先检查7.1.3小节的服务器程序确认已经在运行,并停留在“Waiting for connection...”提示等待客户程序连接。
MySSLClient2运行将出现如下出错信息:
Exception in thread "main" javax.net.ssl.SSLHandshakeException: Couldn't find trusted certificate
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.b(DashoA6275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(DashoA62
75)
at com.sun.net.ssl.internal.ssl.Handshaker.process_record(DashoA6275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.AppInputStream.read(DashoA6275)
at java.io.InputStream.read(InputStream.java:88)
at sun.nio.cs.StreamDecoder$ConverterSD.implRead(StreamDecoder.java:282)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:179)
at java.io.InputStreamReader.read(InputStreamReader.java:167)
at java.io.BufferedReader.fill(BufferedReader.java:136)
at java.io.BufferedReader.readLine(BufferedReader.java:299)
at java.io.BufferedReader.readLine(BufferedReader.java:362)
at MySSLClient2.main(MySSLClient2.java:12)
客户程序既没有在程序中通过System.setProperty( )方法、也没有在运行时通过Java命令选项指明客户程序信任哪个密钥库中的证书,因此运行时将使用默认信任密钥库C:\j2sdk1.4.0\jre\lib\security\cacerts检查客户程序是否信任服务器程序提供的证书。而在默认信任密钥库中既没有服务器提供的“Liu Fang”证书,也没有签发“Liu Fang”证书的CA “Xu Yingxiao”的证书。所以程序抛出异常SSLHandshakeException,提示无法找到信任的证书。
使用keytool工具可以看一下默认密钥库信任哪些证书:
C:\>keytool -list -keystore C:\j2sdk1.4.0\jre\lib\security\cacerts
输入keystore密码: changeit
您的 keystore 包含 10 输入
thawtepersonalfreemailca, 1999-2-13, trustedCertEntry,
认证指纹 (MD5): 1E:74:C3:86:3C:0C:35:C5:3E:C2:7F:EF:3C:AA:3C:D9
thawtepersonalbasicca, 1999-2-13, trustedCertEntry,
认证指纹 (MD5): E6:0B:D2:C9:CA:2D:88:DB:1A:71:0E:4B:78:EB:02:41
verisignclass3ca, 1998-6-30, trustedCertEntry,
认证指纹 (MD5): 78:2A:02:DF:DB:2E:14:D5:A7:5F:0A:DF:B6:8E:9C:5D
thawtepersonalpremiumca, 1999-2-13, trustedCertEntry,
认证指纹 (MD5): 3A:B2:DE:22:9A:20:93:49:F9:ED:C8:D2:8A:E7:68:0D
thawteserverca, 1999-2-13, trustedCertEntry,
认证指纹 (MD5): C5:70:C4:A2:ED:53:78:0C:C8:10:53:81:64:CB:D0:1D
verisignclass4ca, 1998-6-30, trustedCertEntry,
认证指纹 (MD5): 1B:D1:AD:17:8B:7F:22:13:24:F5:26:E2:5D:4E:B9:10
verisignserverca, 1998-6-30, trustedCertEntry,
认证指纹 (MD5): 74:7B:82:03:43:F0:00:9E:6B:B3:EC:47:BF:85:A5:93
verisignclass1ca, 1998-6-30, trustedCertEntry,
认证指纹 (MD5): 51:86:E8:1F:BC:B1:C3:71:B5:18:10:DB:5F:DC:F6:20
thawtepremiumserverca, 1999-2-13, trustedCertEntry,
认证指纹 (MD5): 06:9F:69:79:16:66:90:02:1B:8C:8C:A2:C3:07:6F:3A
verisignclass2ca, 1998-6-30, trustedCertEntry,
认证指纹 (MD5): EC:40:7D:2B:76:52:67:05:2C:EA:F2:3A:4F:65:F0:D8
默认密钥库的初始密码是changeit,需要时可以修改密码。和7.1.2小节的clienttrust密钥库类似,其中存放的只有证书没有对应的私钥,所以每个证书的名字后面显示的是“trustedCertEntry”而不是keyEntry。另外7.1.2小节是通过System.setProperty( )方法、或Java命令选项指定密钥库clienttrust的,而默认密钥库不需要指定。
如果服务器程序使用的证书是这些证书对应的私钥所签发的,则本实例的程序可以直接运行。7.1.3小节服务器程序使用的证书是CA“Xu Yingxiao”签发的,在默认密钥库中不存在其证书,因此需要将CA“Xu Yingxiao”的证书导入该默认密钥库。这里可以使用7.1.2小节所使用的密钥文件mytest.cer,执行如下命令即可。执行命令前可将cacerts文件备份一下。
C:\java\ch7\Client>keytool -import -keystore C:\j2sdk1.4.0\jre\lib\security\cacerts -file mytest.cer -alias mytest
输入keystore密码: changeit
Owner: CN=Xu Yingxiao, OU=Network Center, O=Shanghai University, L=ZB, ST=Shanghai, C=CN
发照者: CN=Xu Yingxiao, OU=Network Center, O=Shanghai University, L=ZB, ST=Shanghai, C=CN
序号: 3deec043
有效期间: Thu Dec 05 10:56:03 CST 2002 至: Sun Nov 17 10:56:03 CST 2013
认证指纹:
MD5: B2:DC:75:CD:60:B7:1E:7A:97:EE:E8:A4:31:D6:26:C6
SHA1: 32:E5:89:16:7E:25:7F:86:16:94:34:36:95:44:D7:CF:14:C8:F2:1E
信任这个认证? [否]: 是
认证已添加至keystore中
这样,只要是mytest.cer证书对应的私钥签发的证书都将自动被信任。如在7.1.3小节的服务器程序已经启动的前提下直接输入“java MySSLClient2”运行程序,则程序将显示“Hi”。
试验完毕可将备份的cacerts恢复,或执行keytool –delete –alias mytest –keystore C:\j2sdk1.4.0\jre\lib\security\cacerts -storepass changeit删除添加的证书。
7.1.5 通过KeyStore对象选择密钥库
★ 实例说明
除了通过System.setProperty( )方法或者Java命令选项指定密钥库及其密码外,还可以在程序中通过KeyStore对象指定密钥库及密码。
在前面各小节的例子中,保护密钥库的密码和各个条目中保护私钥的密码必须相同。使用KeyStore对象指定密钥库及密码时,两种密码可以不同。
★ 编程思路:
可对前面各个小节的例子中创建SSLServerSocketFactory类型对象的方法作些修改,不再通过SSLServerSocketFactory类的静态方法getDefault( ),而通过SSLContext类的getServerSocketFactory( )方法获得SSLServerSocketFactory类型对象,进而获得ServerSocket对象。
在SSLContext类的初始化过程中,可以传入包含密钥库、密钥库口令、私钥口令等信息的KeyManagerFactory对象。具体编程步骤如下:
(1) 获取SSLContext对象
SSLContext context=SSLContext.getInstance("TLS");
分析:通过SSLContext类的getInstance( )方法获得SSLContext类型的对象,方法的参数中指定协议类型,可以是SSL或其低层的TLS等。该步骤得到的SSLContext对象实现了参数中指定的协议。
(2) 获取KeyManagerFactory对象
KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509");
分析:通过KeyManagerFactory类的getInstance( )方法获得KeyManagerFactory类型的对象,方法的参数中指定算法。该步骤得到的KeyManagerFactory对象实现了参数中指定的密钥管理算法。
(3) 获取KeyStore对象
FileInputStream fin=new FileInputStream(storename);
ks=KeyStore.getInstance("JKS");
ks.load(fin,storepass);
分析:和5.2.7小节一样,通过KeyStore类的静态方法getInstace( )获得KeyStore对象,执行其load( )方法加载密钥库,方法的参数指定密钥库的文件输入流和保护密钥库的密码。
(4) 初始化KeyManagerFactory对象
kmf.init(ks,keypass);
分析:执行第二步得到的KeyManagerFactory对象的init( )方法,方法参数传入上一步得到的代表密钥库的KeyStore对象和提取其中的私钥所需要的密码。
(5) 初始化SSLContext对象
context.init(kmf.getKeyManagers(),null,null);
分析:执行第一步得到的SSLContext对象的init( )方法,方法的第一个参数传入上一步得到的KeyManagerFactory对象,其他两个参数暂且设置为null。在后面的内容中将进一步介绍。
(6) 创建SSLServerSocketFactory对象
SSLServerSocketFactory ssf= context.getServerSocketFactory( );
分析:执行第一步得到的SSLContext对象的getServerSocketFactory( )方法,它将创建SSLServerSocketFactory对象,可进而创建ServerSocket对象。
(7) 创建ServerSocket对象
ServerSocket ss=ssf.createServerSocket(5432);
分析:执行上一步得到的ServerSocket对象的createServerSocket ( )方法,它将创建ServerSocket对象,方法的参数指定端口号。从这里往后的编程就和7.1.1、7.1.3完全相同了。
★代码与分析:
完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
import java.security.*;
public class MySSLServerKs {
public static void main(String args[ ])throws Exception {
SSLContext context;
KeyManagerFactory kmf;
KeyStore ks;
char[] storepass="newpass".toCharArray();
char[] keypass="wshr.ut".toCharArray();
String storename="lfnewstore";
context=SSLContext.getInstance("TLS");
kmf=KeyManagerFactory.getInstance("SunX509");
FileInputStream fin=new FileInputStream(storename);
ks=KeyStore.getInstance("JKS");
ks.load(fin,storepass);
kmf.init(ks,keypass);
context.init(kmf.getKeyManagers(),null,null);
SSLServerSocketFactory ssf= context.getServerSocketFactory();
ServerSocket ss=ssf.createServerSocket(5432);
System.out.println("Waiting for connection...");
while(true){
Socket s=ss.accept( );
PrintStream out = new PrintStream(s.getOutputStream( ));
out.println("Hi");
out.close( );
s.close( );
}
}
}
这里不妨使用6.1.2小节得到的密钥库lfnewstore,在6.1.2小节中,该密钥库的保护口令设置为newpass,其中的私钥的保护口令为wshr.ut。
★运行程序
在6.1.2小节中,密钥库lfnewstore中有两个条目lf和lf_signed,如何在选择条目将在本章后面介绍,这里为了简化,不妨删除其中的lf条目。将原有lfnewstore备份后,执行
keytool –delete –alias lf -storepass newpass –keystore lfnewstore
则lfnewstore中将只有一个条目:lf_signed,该条目对应的证书“Liu Fang”是由CA “Xu Yingxiao”签发的。
输入“java MySSLServerKs”运行程序,屏幕出现“”提示后,可再打开一个DOS窗口,执行7.1.2或7.1.3、7.1.4的客户程序。
7.2 进一步的SSL客户和服务器程序的例子
本节在7.1节最简单的例子的基础上给出进一步的例子,包括客户机/服务器的双向通信、查看对方的证书等。
7.2.1 设计通信规则
由7.1节的例子我们已经可以在客户机、服务器程序之间以加密方式交换信息,剩下问题就是客户机和服务器程序在处理输入和输出时按照什么规则进行。这主要取决于具体应用的要求。
★ 实例说明
本实例使用一个简单的规则编写了客户机/服务器双向通信的程序,服务器按照不同的客户请求发送不同文件到客户程序,客户程序根据服务器发来的标题的不同按照不同方式保存文件。
★ 编程思路:
不妨制订如下简单的通信规则:服务器收到客户发来的信息后,如果发现是“.html”结尾,则向客户程序先发送一串信息:“Sending HTML”,再发送一串HTML文本,发送完毕后发送“Session Over” 字符串结束会话。如果发现是“.gif”,则向客户程序先发送一串信息:“Sending GIF”,再发送一个图片文件。
客户机和服务器建立连接后,向服务器发送请求字符串,然后读取服务器反馈信息。若收到“Sending HTML”字符串则建立HTML为后缀的文件,若收到“Sending GIF”字符串则建立.gif为后缀的文件,然后继续读取服务器反馈信息,将读取的内容存入文件。
这样,服务器程序开头部分和7.1.1小节一样,获取Socket类型的对象s,其余部分的编程步骤为:
(1) 获取输出流
OutputStream outs=s.getOutputStream( );
PrintStream out = new PrintStream(outs);
分析:执行Socket对象的getOutputStream( )方法,得到OutputStream类型的对象,通过其write( )方法可以向客户程序发送字节数组。不妨再利用OutputStream对象象创建PrintStream类型的对象,通过其println( )方法可以向客户程序发送字符串。
(2) 获取输入流
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
分析:执行Socket对象的getInputStream( )方法,进而创建BufferedReader类型的对象,通过其readln( )方法可以读取从客户程序发来的字符串。
(3) 读取客户发来的字符串
String line=in.readLine()
分析:执行输入流的readLine( )方法。
(4) 判断客户发来的字符串
if (line.endsWith(".html")){
out.println("Sending HTML");
out.println(…);
}
else if(line.endsWith(".gif")){
out.println("Sending GIF");
out.println(…);
}
分析:根据规则,检查字符串是以“.html”还是.gif结尾,分别向客户程序发送送不同的字符串和不同文件。完整的程序可以根据发来的字符串的不同从文件系统或网络中读取对应的文件提供给客户程序。本实例为简洁起见只向客户程序发送固定的内容。
客户程序开头部分和7.1.2小节一样,获取Socket类型的对象s,其余部分的编程步骤为:
(1) 获取输出流
OutputStream outs=s.getOutputStream( );
PrintStream out = new PrintStream(outs);
分析:和服务器程序一样。
(2) 获取输入流
InputStream ins = s.getInputStream( );
BufferedReader in = new BufferedReader(
new InputStreamReader(ins));
分析:和服务器程序一样。
(3) 向服务器发送字符串
out.println(args[0]);
分析:执行输出流的println( )方法。 不妨从命令行参数读取相应的字符串,该字符串中可以指定要读取什么样的内容。只要客户程序和服务器程序编程之前约定好了字符串的格式和含义,字符串可以是任意格式。
(4) 接收服务器反馈信息
String x=in.readLine( );
分析:执行输入流的readLine( )方法。
(5) 根据服务器的反馈信息打开不同的文件输出流
if( x.equals("Sending HTML")){
fouts=new FileOutputStream("result.html");
}
else if( x.equals("Sending GIF")){
fouts=new FileOutputStream("result.gif");
}
分析:如果服务器发来“Sending HTML”,则创建文件名以.html为后缀的文件; 如果服务器发来“Sending GIF”,则创建文件名以.gif为后缀的文件。
(6) 接收服务器进一步的反馈信息
while((kk=ins.read())!=-1){
System.out.println(kk);
fouts.write(kk);
}
分析:执行输入流的read( )方法读取服务器发来的信息,将服务器发来的文件内容存入上一步对应的文件。
★代码与分析:
服务器完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MySSLServerRule{
public static void main(String args[ ]) throws Exception{
byte[] x={
(byte)0x47,(byte)0x49,(byte)0x46,(byte)0x38,(byte)0x39,
(byte)0x61,(byte)0x05,(byte)0x00,(byte)0x05,(byte)0x00,
(byte)0x80,(byte)0xff,(byte)0x00,(byte)0xff,(byte)0xff,
(byte)0xff,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x2c,
(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x05,
(byte)0x00,(byte)0x05,(byte)0x00,(byte)0x40,(byte)0x02,
(byte)0x07,(byte)0x44,(byte)0x0e,(byte)0x86,(byte)0xc7,
(byte)0xed,(byte)0x51,(byte)0x00,(byte)0x00,(byte)0x3b
};
System.setProperty("javax.net.ssl.keyStore","mykeystore");
System.setProperty("javax.net.ssl.keyStorePassword","wshr.ut");
SSLServerSocketFactory ssf=
(SSLServerSocketFactory)
SSLServerSocketFactory.getDefault( );
ServerSocket ss=ssf.createServerSocket(5432);
System.out.println("Waiting for connection...");
while(true){
Socket s=ss.accept( );
OutputStream outs=s.getOutputStream( );
PrintStream out = new PrintStream(outs);
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
String line=in.readLine();
System.out.println("Got "+line);
if (line.endsWith(".html")){
System.out.println("Now Sending HTML");
out.println("Sending HTML");
out.println("<HTML><HEAD><TITLE>Test SSL</TITLE></HEAD>");
out.println("<BODY><h1> This is a test</BODY>");
out.println("</HTML>");
}
else if(line.endsWith(".gif")){
System.out.println("Now Sending GIF");
out.println("Sending GIF");
outs.write(x);
out.println("");
}
out.close( );
s.close( );
}
}
}
其中字节数组x中存放的是一个图片文件的十六进制代码,该图片很小,显示一个小圆点。如果程序稍改复杂点,可以根据客户程序发来的字符串直接从文件系统或网络中读取所需的HTML文件或图片。
客户程序完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MySSLClientRule{
public static void main(String args[ ]) throws Exception {
FileOutputStream fouts=null;
System.setProperty("javax.net.ssl.trustStore",
"clienttrust");
SSLSocketFactory ssf=
(SSLSocketFactory) SSLSocketFactory.getDefault( );
Socket s = ssf.createSocket("127.0.0.1", 5432);
OutputStream outs=s.getOutputStream( );
PrintStream out = new PrintStream(outs);
InputStream ins = s.getInputStream( );
BufferedReader in = new BufferedReader(
new InputStreamReader(ins));
out.println(args[0]);
System.out.println("Sent");
String x=in.readLine( );
System.out.println(x);
if( x.equals("Sending HTML")){
fouts=new FileOutputStream("result.html");
}
else if( x.equals("Sending GIF")){
fouts=new FileOutputStream("result.gif");
}
int kk;
while((kk=ins.read())!=-1){
fouts.write(kk);
}
in.close( );
fouts.close();
}
}
★运行程序
和7.1节的程序一样,服务器程序使用密钥库mykeystore中的证书向客户程序证明自己,客户程序使用密钥库clienttrust向服务器程序表明自己是否信任服务器程序提供的证书。
输入“java MySSLServerRule”启动服务器程序,经过一段较长时间的初始化工作,屏幕上显示“Waiting for connection...”,表明服务器准备就绪。
在计算机中再打开一个DOS窗口,输入“java MySSLClientRule http://www/x.gif”运行程序,这里通过命令行参数向服务器提供了字符串“http://www/x.gif”,要求获取该资源。由于我们的服务器程序作了许多简化,只检查字符串的后缀,因此这个字符串可以使用任意格式,只要最后几个字符为.gif或.html即可。如果对服务器程序作些改进,解析该字符串,则可以实现更多功能。
经过一段较长时间的初始化工作,客户机和服务器开始了双向通信,服务器程序的DOS窗口显示:
Got http://www/x.html
Now Sending HTML
客户程序的DOS窗口显示:
Sent
Sending GIF
在运行客户程序的当前目录下将出现一个文件:result.gif,打开该文件将看到服务器程序发来的图片:一个圆点。
如果输入“java MySSLClientRule http://www/test.html”运行程序,则服务器的DOS窗口将显示:
Got http://www/test.html
Now Sending HTML
客户程序的DOS窗口显示:
Sent
Sending HTML
在运行客户程序的当前目录下将出现一个文件:result.html,打开该文件将看到服务器程序发来的网页。
和7.1小节的程序一样,可以删除程序中System.setProperty( )代码,而通过java命令选项来指定系统参数。
7.2.2 查看对方的证书等连接信息
本章所有程序(不论是客户程序还是服务器程序)在得到Socket类型的对象后,都可以查看所连接的对方的证书。
★ 实例说明
本实例演示了如何修改7.1节的客户程序和服务器程序以查看对方的证书等连接信息。
★ 编程思路:
客户程序和服务器程序最终都是通过Socket对象得到输入/输出流而进行双向通信。通过Socket对象不仅可以得到输入/输出流,还可以得到SSLSession类型的对象,该对象描述了连接双方的关系,通过其方法可以获得连接双方的信息。
在得到Socket对象后,客户程序和服务器程序的编程方法相同,如下:
(1) 获取SSLSession对象
SSLSession session=((SSLSocket) s).getSession();
分析:先将Socket对象s强制转换为SSLSocket类型,再执行其getSession( )方法,得到SSLSession对象。以后就可以根据需要有选择地执行该对象的各个方法,如下面各步骤。
(2) 获取对方在SSL协议握手阶段所使用的证书
Certificate[ ] cchain=session.getPeerCertificates( );
分析:执行SSLSession对象的getPeerCertificates( )方法可以获得对方在SSL协议握手阶段所使用的证书,如果对方使用的是证书链,将得到一组证书,存放在Certificate类型的数组中。对数组中的证书可以按照前面几章的方法进行显示、验证等。
(3) 获取自己在SSL协议握手阶段所使用的证书
Certificate[ ] cchain2=session.getLocalCertificates();
分析:执行SSLSession对象的getLocalCertificates( )方法可以获得自己在SSL协议握手阶段所使用的证书,如果对方使用的是证书链,将得到一组证书,存放在Certificate类型的数组中。对数组中的证书可以按照前面几章的方法进行显示、验证等。
(4) 获取对方的主机名称
session.getPeerHost( );
分析:执行SSLSession对象的getPeerHost( )方法可以获得对方的主机名称,如果无法得到对方的主机名称,则得到的是对方的IP地址。
(5) 获取SSL密码组名称
session.getCipherSuite( )
分析:执行SSLSession对象的getCipherSuite( )方法可以获得该会话中所有连接所使用的SSL密码组(cipher suite)的名称。
(6) 获取会话所使用的协议
session. getProtocol( )
分析:执行SSLSession对象的getProtocol( )方法可以获得该会话中所有连接所使用的协议名称。
(7) 获取会话标志符
session.getId( )
分析:每个会话有一个标识符,执行SSLSession对象的getId( )方法可以获得该会话标识符。它返回byte类型的数组。
(8) 获取会话创建时间
session. getCreationTime( )
分析:它返回一个长整型数,表示会话创建时离格林威治标准时间1970年1月1日0时0分0秒相隔多少毫秒 。
(9) 获取会话上次访问时间
session. getLastAccessedTime()
分析:它返回一个长整型数,表示上一次访问该会话时离格林威治标准时间1970年1月1日0时0分0秒相隔多少毫秒。这种访问是指会话级的访问,获取这些时间可以用于会话的管理。
★代码与分析:
服务器程序和7.1节的各个服务器程序类似,只是加上本小节增加的几步,完整代码如下:
import java.net.*;
import java.math.*;
import java.io.*;
import javax.net.ssl.*;
import java.security.cert.*;
public class MySSLServerSession{
public static void main(String args[ ]) throws Exception{
System.setProperty("javax.net.ssl.keyStore","lfkeystore2");
System.setProperty("javax.net.ssl.keyStorePassword","wshr.ut");
SSLServerSocketFactory ssf=(SSLServerSocketFactory)
SSLServerSocketFactory.getDefault( );
ServerSocket ss=ssf.createServerSocket(5432);
System.out.println("Waiting for connection...");
while(true){
Socket s=ss.accept( );
SSLSession session=((SSLSocket) s).getSession();
Certificate[ ] cchain2=session.getLocalCertificates();
System.out.println("The Certificates used in local");
for(int i=0;i<cchain2.length;i++){
System.out.println(((X509Certificate)cchain2[i]).
getSubjectDN() );
}
System.out.println("Peer host is "+session.getPeerHost());
System.out.println("Cipher is "+session.getCipherSuite() );
System.out.println("Protocol is "+session.getProtocol() );
System.out.println("ID is "+
new BigInteger(session.getId()) );
System.out.println("Session created in "+
session.getCreationTime() );
System.out.println("Session accessed in "+
session.getLastAccessedTime());
PrintStream out = new PrintStream(s.getOutputStream( ));
out.println("Hi");
out.close( );
s.close( );
}
}
}
该服务器程序可以和7.1节的各个客户程序进行通信,也可以和本小节下面的客户程序通信。程序运行时将显示服务器程序实际使用的是哪个证书以及会话信息。对于证书,这里简单地将证书链中各个证书的名称打印出来。由于7.1节的各个客户程序只指定了信任哪些证书,并没有提供自己的证书给服务器程序,因此该服务器程序没有使用session.getPeerCertificates( ) 获取对方在SSL协议握手阶段所使用的证书。对于字节数组类型的会话标志符,程序中使用BigInteger类将其转换成长整型再打印出来。
客户程序和7.1节的各个客户程序类似,只是加上本小节增加的几步,完整代码如下,它既可以和7.1节的各个服务器程序通信,也可和本小节的服务器程序通信。
import java.net.*;
import java.math.*;
import java.io.*;
import javax.net.ssl.*;
import java.security.cert.*;
public class MySSLClientSession{
public static void main(String args[ ]) throws Exception {
System.setProperty("javax.net.ssl.trustStore",
"clienttrust");
SSLSocketFactory ssf=
(SSLSocketFactory) SSLSocketFactory.getDefault( );
Socket s = ssf.createSocket("127.0.0.1", 5432);
SSLSession session=((SSLSocket) s).getSession();
Certificate[ ] cchain=session.getPeerCertificates();
System.out.println("The Certificates used by peer");
for(int i=0;i<cchain.length;i++){
System.out.println( ((X509Certificate)cchain[i]).
getSubjectDN() );
}
System.out.println("Peer host is "+session.getPeerHost());
System.out.println("Cipher is "+session.getCipherSuite() );
System.out.println("Protocol is "+session.getProtocol() );
System.out.println("ID is "+new BigInteger(session.getId()) );
System.out.println("Session created in "+
session.getCreationTime() );
System.out.println("Session accessed in "+
session.getLastAccessedTime());
BufferedReader in
= new BufferedReader(new InputStreamReader(s.getInputStream( )));
String x=in.readLine( );
System.out.println(x);
in.close( );
}
}
程序运行时将显示所连接的服务器实际使用的是哪个证书以及会话信息。对于证书,这里简单地将证书链中各个证书的名称打印出来。由于该客户只指定了信任哪些证书,并没有提供自己的证书给服务器程序,因此该服务器程序没有使用session.getLocalCertificates( ) 获取自己在SSL协议握手阶段所使用的证书。对于字节数组类型的会话标志符,程序中使用BigInteger类将其转换成长整型再打印出来。
★运行程序
输入“java MySSLServerSession”运行服务器程序,然后输入“java MySSLClientSession”运行本实例中的客户程序,则服务器程序的DOS窗口显示:
The Certificates used in local
CN=Liu Fang, OU=Packaging, O=Shanghai University, L=ZB, ST=Shanghai, C=CN
CN=Xu Yingxiao, OU=Network Center, O=Shanghai University, L=ZB, ST=Shanghai, C=CN
Peer host is 127.0.0.1
Cipher is SSL_RSA_WITH_RC4_128_SHA
Protocol is TLSv1
ID is 28013371676272649684477370449682354285170795912842258606573317487778178237440
Session created in 1039073705950
Session accessed in 1039073706610
客户程序的DOS窗口显示:
The Certificates used by peer
CN=Liu Fang, OU=Packaging, O=Shanghai University, L=ZB, ST=Shanghai, C=CN
CN=Xu Yingxiao, OU=Network Center, O=Shanghai University, L=ZB, ST=Shanghai, CN
Peer host is localhost
Cipher is SSL_RSA_WITH_RC4_128_SHA
Protocol is TLSv1
ID is 280133716762726496844773704496823542851707959128422586065733174877781782440
Session created in 1039073706000
Session accessed in 1039073706610
Hi
从中可以看出客户程序和服务器程序对同一个会话显示出的服务器所使用的证书、ID、创建时间、协议、加密器组等信息是一致的,而上次访问时间等则不同。
服务器程序或客户程序可以和7.1节中的各个服务器程序和客户程序替换,这样可以看到使用不同的证书的效果。
7.3 HTTPS客户及服务器程序
7.2.1小节设计了自己的通信规则进行双向通信,其实现有的各种网络流量都可以通过SSL进行加密,本章下面开始介绍使用SSL加密HTTP流量的HTTPS。
7.3.1 最简单的HTTPS服务器程序
★ 实例说明
本实例给出最简单的HTTPS服务器程序的例子,它可以通过浏览器来访问,也可以通过本节后面的HTTPS客户程序来访问。
★ 编程思路:
服务器程序开头部分和7.1.1小节一样,获取Socket类型的对象s,其中HTTPS使用的标准端口号是443,因此服务器程序应该使用该端口号,这样浏览器就可以输入“https://服务器地址”来访问服务器了,如果服务器程序中的端口号不是使用443,则浏览器访问时应通过“https://服务器地址:段口号”来访问服务器。
获得Socket类型对象后,就可以和以前一样得到输入/输出流,不同的只是输入/输出流的处理方式不一样,要按照HTTP协议规定的方式进行通信。
HTTP协议基本的规则是:服务器先读取浏览器发来的数据(类似“GET /test.html HTTP/1.1”的字符串),如果是GET请求,则根据请求的内容,向浏览器发送HTTP版本、MIME版本、数据类型、数据长度、一个空行以及数据内容等信息。浏览器只有接收到这样的信息,才认为HTTP协议执行正确,从而将后面发送的内容作为网页内容显示出来。如对获取网页(HTML文档)的请求,Web服务器可以发送如下信息:
"HTTP/1.0 200 OK"
"MIME_version:1.0"
"Content_Type:text/html"
"Content_Length:"+c.length( )
""
c
其中字符串c中包含的是网页的内容。
按照这样的流程,具体的编程步骤可以如下:
(1) 获取输出流
PrintStream out = new PrintStream(s.getOutputStream( ));
分析:执行Socket对象的getOutputStream( )方法,得到OutputStream类型的对象,通过其write( )方法可以向客户程序发送字节数组。不妨再利用OutputStream对象象创建PrintStream类型的对象,通过其println( )方法可以向客户程序发送字符串。
(2) 获取输入流
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
分析:执行Socket对象的getInputStream( )方法,进而创建BufferedReader类型的对象,通过其readln( )方法可以读取从客户程序发来的字符串。
(3) 读取客户发来的字符串
while(( info=in.readLine())!=null){
System.out.println("now got "+info);
if(info.equals("")) break;
}
分析:浏览器要访问Web服务器之前会先发送用户浏览器版本、语言、要访问的内容等信息,因此服务器程序循环读取浏览器发来的信息,直到读到空字符串或读完为止。
(4) 向客户发送信息
out.println("HTTP/1.0 200 OK");
out.println("MIME_version:1.0");
out.println("Content_Type:text/html");
分析:根据HTTP协议的规定,Web服务器收到浏览器发来的请求后,会通知浏览器HTTP版本、MIME版本、数据类型等信息,通过输出流的打印语句将这些信息发送给浏览器或客户程序。
(5) 获取要传输的内容
i++;
String c="<html> <head></head><body> <h1> Hi, this is "
+i+"</h1></Body></html>";
分析:这里为简化程序,将要发送给浏览器的网页内容放在一个字符串中,该字符串中的网页显示“Hi, this is ”信息,末尾用一个变量显示一个数字,这个数字每次向浏览器发送信息时增加1,这样造成动态效果。第一次发送网页时显示“Hi, this is 1”,第二次发送网页时则显示“Hi, this is 2”,字符串中添加了很多制作网页所用的HTML标记控制显示的格式。
实际使用中一般可根据浏览器发来的请求读取指定目录的文件发送给浏览器。除了网页外,也可以发送图片等内容。
(6) 发送网页
out.println("Content_Length:"+c.length( ));
out.println("");
out.println(c);
分析:先向浏览器发送网页的长度信息,然后将上一步得到的内容发送出去。
★代码与分析:
完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MyHTTPSServer {
public static void main(String args[ ]) {
int i=0;
try {
SSLServerSocketFactory ssf= (SSLServerSocketFactory)
SSLServerSocketFactory.getDefault( );
ServerSocket ss=ssf.createServerSocket(443);
System.out.println("Web Server OK ");
while(true){
Socket s=ss.accept( ); //等待请求
PrintStream out = new PrintStream(s.getOutputStream( ));
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
String info=null;
while(( info=in.readLine())!=null){
System.out.println("now got "+info);
if(info.equals("")) break;
}
System.out.println("now go");
out.println("HTTP/1.0 200 OK");
out.println("MIME_version:1.0");
out.println("Content_Type:text/html");
i++;
String c="<html> <head></head><body> <h1> Hi, this is "
+i+"</h1></Body></html>";
out.println("Content_Length:"+c.length( ));
out.println("");
out.println(c);
out.close( );
s.close( );
in.close( );
}
} catch (IOException e) {
System.out.println(e);
}
}
}
★运行程序
如下运行程序,至屏幕上显示“Web Server OK”,开始可以接收浏览器的连接。
C:\java\ch7\Server>java -Djavax.net.ssl.keyStore=lfkeystore2 -Djavax.net.ssl.keyStorePassword=wshr.ut MyHTTPSServer
然后再另外一台联网的计算机,或在运行MyHTTPSServer程序的同一台计算机上打开浏览器,输入“https://服务器地址”,如“https:/127.0.0.1”。则浏览器将出现图 7- 1所示的提示:
该提示信息表明浏览器即将访问HTTPS服务器,浏览器和服务器之间传递的信息将加密处理。点击“确定”按钮后,浏览器将读取HTTPS服务器提供的证书,如图 7- 2所示,
由于本实例HTTPS服务器使用的是密钥库lfkeystore2中的“Liu Fang”证书,在用户的浏览器中既没有信任该证书,也没有信任该证书的签发者CA “Xu Yingxiao”的证书,因此这里给出警告信息:“该安全证书由您没有选定信任的公司颁发”。如果按照5.4.1小节的方法安装CA “Xu Yingxiao”的证书mytest.cer或“Liu Fang”的证书lf_signed.cer,或直接点击图 7- 2提示中的“安装证书”按钮,在出现的图中类似5.4.1小节图5-5的窗口中点击“安装证书”,使得用户计算机信任该证书,则将不出现该提示。同样,如果HTTPS服务器使用的证书不是我们自己签发的,而是交给著名CA如Verisign签发(要付费),则浏览器也将自动信任HTTPS服务器的证书。
此外这里还检测证书的日期是否有效、证书上的名称与站点名称是否匹配等。图 7- 2中显示证书上的名称与站点名称不匹配,这是因为证书中的名称是“Liu Fang”,而访问该站点时我们使用了127.0.0.1来访问这个站点。如果创建证书时使用服务器的IP地址或者域名作为证书的名称,则该警告信息也将不再出现。
对于上面两个警告信息,点击“是”按钮,确定尽管有这两个警告,用户经过检查还是觉得浏览时信任该证书,于是可以看到网页内容,如图 7- 3所示
“Hi, this is 2”是由本实例的HTTPS服务器加密传送给浏览器的,如果点击浏览器“刷新”按钮,则继续出现 “Hi, this is 3”,“Hi, this is 4”等内容。
Web服务器上提示如下:
Web Server OK
now go
now got GET / HTTP/1.1
now got Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/msword, application/vnd.ms-powerpoint, */*
now got Accept-Language: zh-cn
now got Accept-Encoding: gzip, deflate
now got User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt)
now got Host: 127.0.0.1
now got Connection: Keep-Alive
now got Accept-Language: zh-cn
now got Accept-Encoding: gzip, deflate
now got
now go
下面我们重新创建一个证书,使得证书中宣称的名称和服务器实际的名称一致。将如下命令保存在c:\java\ch7\server目录comstore.bat文件中,并执行该批处理文件。
keytool -genkey -dname "CN=www.my.com, OU=NC, O=Shanghai University, L=ZB, ST=Shanghai, C=CN" -alias my -keyalg RSA -keystore mycomstore -keypass wshr.ut -storepass wshr.ut -validity 1000
该命令将在当前目录下创建文件名为mycomstore的密钥库文件,其中条目my存放一个名称为www.my.com的证书。
此时,可以使用该证书运行服务器程序:
java -Djavax.net.ssl.keyStore=mycomstore -Djavax.net.ssl.keyStorePassword=wshr.ut MyHTTPSServer
浏览器访问该服务器时,应该输入https://www.my.com,这样就不会出现“证书上的名称与站点名称不匹配”的警告了。为了使用户能通过www.my.com的名称访问到该服务器,应该为该服务器的IP地址设置www.my.com的域名。小范围内使用时也可以不设置域名,而是在用户机器的Hosts文件中给服务器的IP地址设置“www.my.com”的主机名。
如本实例的服务器程序若运行在IP地址为202.120.1.1的计算机上,用户在另外一台联网的计算机B上通过浏览器访问该服务器,则假设计算机B安装的是Windows9X操作系统,则用户可以在计算机B的c:\windows目录创建一个文本文件:hosts。 该文件内容中如下:
202.120.1.1 www.my.com
该段内容必须顶格写,前面不能有空格,则以后该用户访问www.my.com时将访问IP地址为202.120.1.1的计算机。
如果只有一台计算机,也可以使用IP地址127.0.0.1代表本台机器。如可以在hosts文件中加入如下内容:
127.0.0.1 www.my.com
这样,浏览器中输入:https://www.my.com,将出现图 7- 4所示的窗口。
和图 7- 2相比,该窗口中已经不再有“证书上的名称与站点名称不匹配”的警告了。
下面我们按照使用5.4.3小节的程序签发mycomstore中的证书,并按照6.1.1小节的方法将签发后的证书导入密钥库。
先建立一个临时目录tmp,其中拷贝刚才创建的密钥库mycomstore、5.4.3小节的程序SignCert、以及CA“Xu Yingxiao”的密钥库mykeystore。
执行如下命令将mycomstore中的证书导出到文件:
keytool –export -alias my –file www.my.com.cer -keystore mycomstore –storepass wshr.ut
执行如下命令将mykeystore中CA“Xu Yingxiao”的证书导出到文件:
keytool –export -alias mytest –file mytest.cer -keystore mykeystore –storepass wshr.ut
执行“java SignCert www.my.com.cer”,则得到密钥库newstore,密码为newpass,签发后的证书保存在条目lf_signed中。执行如下命令将签发后的证书导出到文件mycomsigned.cer:
keytool -export -file mycomsigned.cer -keystore newstore -storepass newpass -alias lf_signed
再执行如下命令将CA“Xu Yingxiao”的证书mytest.cer导入到密钥库mycomstore。
keytool -import -alias CAmytest -keystore mycomstore -file mytest.cer –storepass wshr.ut
最后执行
keytool -import -alias my -keystore mycomstore -file mycomsigned.cer –storepass wshr.ut
导入签名后的证书。
不妨将mycomstore文件备份为mycomstore2,拷贝到c:\java\ch7\server目录。这样,我们得到了又一个由CA“Xu Yingxiao”签名的证书,它的证书链以条目my保存在密钥库mycomstore2中。
同样,我们可以输入
java -Djavax.net.ssl.keyStore=mycomstore2 -Djavax.net.ssl.keyStorePassword=wshr.ut MyHTTPSServer
运行程序,如果用户的计算机上已经如5.4.1小节安装过mytest.cer或mycomsigned.cer证书,则输入https://www.my.com浏览时将直接看到网页,而不再出现图 7- 2或图 7- 3所示的警告窗口。
如果使用抓包软件捕捉浏览器和服务器之间的通信,可以发现网页的内容在网络中传递时是以密文传递的。
7.3.2 最简单的HTTPS客户程序
★ 实例说明
本实例使用URL类按照HTTPS方式和7.3.1小节的HTTPS服务器以及网上已有的支持HTTPS的Web服务器进行加密的通信。本实例的程序也可用于不加密的以HTTP方式访问的Web服务器。
★ 编程思路:
使用java.net包中的URL类可以根据“http://…”等形式的地址访问对应的Web站点上的网页、图片或其他Internet资源。首先生成URL类型的对象,然后通过其生成输入流,最后通过对输入流的操作获得所需要的资源。
具体步骤如下:
(1) 创建URL类型的对象
URL u = new URL(args[0]);
分析: 使用java.net包中的URL类,其参数为字符串类型,代表所要访问的资源的地址(如http://www.shu.edu.cn/~xyx),这里 不妨从命令行参数读入所要访问的地址。
(2) 获取输入流
InputStream in = u.openStream( );
分析: 执行URL对象的openStream( )方法得到对应该URL的输入流,以后通过该输入流可以访问URL对应的资源。
(3) 处理输入流
BufferedReader f= new BufferedReader(new InputStreamReader (in));
fileline = f.readLine( );
分析: 可以按照传统的输入流的各种使用方法从输入URL对应的输入流中读取相应的数据。如可利用它创建BufferedReader类型的对象,然后使用其readLine( )方法一行一行读取网页内容。如果第1步中传入的是HTTPS开头的字符串,则程序内部将自动使用HTTPS协议和Web服务器进行通信,所有数据在网上传递时已经加密过,在程序内部和Web服务器内部自动解密。
★代码与分析:
完整代码如下:
import java.io.*;
import java.net.*;
class HttpsClient {
public static void main(String args[ ]) throws IOException{
String line;
URL u = new URL(args[0]);
InputStream in = u.openStream( );
BufferedReader f= new BufferedReader(new InputStreamReader (in));
while ((line = f.readLine( )) != null) {
System.out.println(line+"\n");
}
}
}
本程序是针对J2SDK1.4版本的,如果使用的是老版本的JDK,需要增加两行语句:
System.setProperty("java.protocol.handler.pkgs",
"com.sun.net.ssl.internal.www.protocol");
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
才可以使用HTTPS。
★运行程序
如果计算机已经联网,可以使用网上已有的Web服务器运行本程序。
首先测试不加密的方式,输入“java HttpsClient http://www.shu.edu.cn/~xyx > my.html”运行程序,则可以访问http://www.shu.edu.cn/~xyx所对应的网页内容,这里将屏幕输出重定向到了网页my.html中,可以用浏览器打开网页查看网页内容。
同样,输入“java HttpsClient http://www.shu.edu.cn/~xyx/cindex.html > my2.html”运行程序,则可以访问http://www.shu.edu.cn/~xyx/cindex.html所对应的网页内容。
如果使用抓包软件捕捉运行该Java程序的计算机的通信,可以发现网页的内容在网络中传递时是以明文传递的。
下面测试使用HTTPS的加密方式,这需要所连接的服务器支持HTTPS。只要找一些https开头的网址即可,如“java HttpsClient https://intranet.ied.edu.hk >uk.html”。
由于intranet.ied.edu.hk网站所使用的证书是客户机默认密钥库C:\j2sdk1.4.0\jre\lib\security\cacerts中的CA或其下级机构所签发的,因而可以通过验证,uk.html文件中将得到https://intranet.ied.edu.hk网站的内容。
如果客户机不信任服务器所使用的证书,则无法通过https通信。如使用本实例连接7.3.1小节的HTTPS服务器,如果输入“java HttpsClient https://127.0.0.1”连接7.3.1小节的服务器程序(如果HttpsClient和7.3.1小节的程序不在同一台计算机上,应该把其中的127.0.0.1改为7.3.1小节程序所在计算机的实际IP地址或域名)。则程序出现如下错误提示:
Exception in thread "main" javax.net.ssl.SSLHandshakeException: Couldn't find tr
usted certificate
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.b(DashoA6275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(DashoA6275)
at com.sun.net.ssl.internal.ssl.Handshaker.process_record(DashoA6275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.AppOutputStream.write(DashoA6275)
at java.io.OutputStream.write(OutputStream.java:58)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(DashoA6275)
at sun.net.www.protocol.https.HttpsClient.afterConnect(DashoA6275)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect (DashoA6275)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream (HttpURLConnection.java:556)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream (DashoA6275)
at java.net.URL.openStream(URL.java:955)
at HttpsClient.main(HttpsClient.java:13)
这是因为7.3.1小节的HTTPS服务器所使用的证书(密钥库lfkeystore2中的“Liu Fang”证书或mycomstore2中的证书“www.my.com”)及其签发者都没有放在客户Java程序默认的信任密钥库C:\j2sdk1.4.0\jre\lib\security\cacerts文件中,因此客户程序不信任服务器所使用的证书。在7.3.1小节运行程序时,我们在浏览器中通过弹出的警告窗口手工确认信任服务器所使用的证书,或在Windows中安装证书表明浏览器信任哪些证书,而本实例则可以在运行时通过Java命令选项指定。
类似7.1.2小节,输入如下命令执行程序:
java -Djavax.net.ssl.trustStore=clienttrust HttpsClient https://127.0.0.1
该命令指定程序信任密钥库clienttrust中的证书,该密钥库是7.1.2小节创建的,包含了证书“Liu Fang”及“www.my.com”的签发者“Xu Yingxiao”的证书:mytest.cer。
此时不再出现找不到信任的证书的提示,而是出现如下出错提示:
C:\java\ch7\Client>java -Djavax.net.ssl.trustStore=clienttrust HttpsClient https://127.0.0.1
Exception in thread "main" java.io.IOException: HTTPS hostname wrong: should be <127.0.0.1>
at sun.net.www.protocol.https.HttpsClient.b(DashoA6275)
at sun.net.www.protocol.https.HttpsClient.afterConnect(DashoA6275)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect (DashoA6275)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:556)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(DashoA6275)
at java.net.URL.openStream(URL.java:955)
at HttpsClient.main(HttpsClient.java:13)
这是因为客户程序是通过127.0.0.1来访问服务器程序的,而服务器程序的证书使用的不是这个名字。因此7.3.1小节中的服务器必须使用“java -Djavax.net.ssl.keyStore=mycomstore2 -Djavax.net.ssl.keyStorePassword=wshr.ut MyHTTPSServer”来运行,而本小节的程序必须使用“java -Djavax.net.ssl.trustStore=clienttrust HttpsClient https://www.my.com”来运行。此时,屏幕输出如下:
C:\java\ch7\Client>java -Djavax.net.ssl.trustStore=clienttrust HttpsClient https://www.my.com
<html> <head></head><body> <h1> Hi, this is 5</h1></Body></html>
此外,客户程序也可以像7.1.4小节使用默认信任密钥库或7.1.2小节使用System.setProperty( )语句设置信任的证书。
如果使用抓包软件捕捉运行该Java程序的计算机的通信,可以发现网页的内容在网络中传递时是以加密方式传递的。
7.3.3 基于Socket的HTTPS客户程序
★ 实例说明
本实例从更低层使用Socket类和7.3.1小节以及Internet上其他的HTTPS服务器通信。
★ 编程思路:
和7.1.2及7.2.1小节一样,先获取Socket类型的对象s,其中HTTPS使用的标准端口号是443,因此连接服务器时应该使用该端口号,这样客户程序就可以和7.3.1小节的程序服务器程序中的端口号不是使用443,则浏览器访问时应通过“https://服务器地址:段口号”来访问服务器。
获得Socket类型对象后,就可以和以前一样得到输入/输出流,不同的只是输入/输出流的处理方式不一样,要按照HTTP协议规定的方式进行通信。
具体的编程步骤可以如下:
(1) 获取输出流
OutputStream outs=s.getOutputStream( );
PrintStream out = new PrintStream(outs);
分析:执行Socket对象的getOutputStream( )方法,得到OutputStream类型的对象,进而得到PrintStream类型的对象,通过其println( )方法可以向HTTPS服务器发送字符串。
(2) 获取输入流
InputStream ins = s.getInputStream( );
BufferedReader in = new BufferedReader(
new InputStreamReader(ins));
分析:执行Socket对象的getInputStream( )方法,得到InputStream类型的对象,进而得到BufferedReader类型的对象,通过其readLine( )方法可以读取HTTPS服务器发来的字符串。
(3) 向服务器发送字符串
out.println("Hi,How are u!");
分析:执行输出流的println( )方法。完整的HTTP协议中包含了一系列内容,这里不妨只发送一串。
(4) 接收服务器反馈信息
while((line=in.readLine())!=null){
System.out.println(line);
}
分析:执行输入流的readLine( )方法。
★代码与分析:
完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class HttpsSocketClient {
public static void main(String args[ ])throws Exception {
try {
int port = 443;
System.setProperty("javax.net.ssl.trustStore",
"clienttrust");
String hostname = args[0];
SSLSocketFactory ssf=
(SSLSocketFactory) SSLSocketFactory.getDefault( );
Socket s = ssf.createSocket(hostname, port);
OutputStream outs=s.getOutputStream( );
PrintStream out = new PrintStream(outs);
InputStream ins = s.getInputStream( );
BufferedReader in = new BufferedReader(
new InputStreamReader(ins));
out.println("Hi,How are u!");
out.println("");
String line=null;
while((line=in.readLine())!=null){
System.out.println(line);
}
in.close();
out.close();
} catch(IOException e) {
}
}
}
★运行程序
如果机器已经联网,可以使用网上已有的支持HTTPS的Web服务器测试程序。也可以连接7.3.1所示的服务器程序。执行“java HttpsSocketClient 127.0.0.1”,将得到如下HTTPS服务器反馈的信息:
HTTP/1.0 200 OK
MIME_version:1.0
Content_Type:text/html
Content_Length:65
<html> <head></head><body> <h1> Hi, this is 1</h1></Body></html>
7.3.4 传输实际文件
★ 实例说明
7.3.1小节的例子中,服务器程序为了简化,只向浏览器传输Hi信息。本实例修改了7.3.1小节最简单的HTTPS服务器程序,使其支持实际的文件传输。
★ 编程思路:
将7.3.1小节的程序MyHTTPSServer中向浏览器发送数据部分:
i++;
String c="<html> <head></head><body> <h1> Hi, this is "
+i+"</h1></Body></html>";
out.println("Content_Length:"+c.length( ));
out.println("");
out.println(c);
替换掉。
在浏览器中输入“http://127.0.0.1/xx/yy.html”等字符串,从HTTPS服务程序显示的信息可发现,服务器接收到的是“GET /xx/yy.html HTTP/1.1”字符串。因此,只要对该字符串进行解析,获取文件的相对目录和文件名,便可以将文件内容发送到用户浏览器。
HTTPS服务器接收到的字符串中,文件相对路径和名称在第一个和第二个空格之间,因此,通过字符串的indexOf( )方法获得第一个和第二个空格的位置后,便可以通过字符串的substring( )方法提取出相对路径和文件名:
int sp1=request.indexOf(' ');
int sp2=request.indexOf(' ',sp1+1);
String filename=request.substring(sp1+2,sp2);
因为浏览器中输入的内容有时不带文件名,如输入http://127.0.0.1和http://127.0.0.1/t/时,Web服务器获得的字符串分别为“GET / HTTP/1.1”和“GET /t/ HTTP/1.1”,这样提取出的内容分别为空字符串和“t/”,因此这时应为其加上默认文件名:
if(filename.equals("") || filename.endsWith("/")){
filename+="index.html";
}
最后通过文件输入流读取文件内容,将其发送到浏览器:
File fi=new File(filename);
InputStream fs=new FileInputStream(fi);
int n=fs.available( );
byte buf[ ]=new byte[1024];
out.println("Content_Length:"+n);
out.println("");
while ((n=fs.read(buf))>=0){
out.write(buf,0,n);
}
进一步还可检测文件是否存在,不存在则发送出错信息。发送出错信息的代码如下:
String c="<html><head><title>Not Found</title></head><body>”
+”<h1>Error 404-file not found</h1></body></html>";
outstream.println("HTTP/1.0 404 no found");
outstream.println("Content_Type:text/html");
outstream.println("Content_Length:"+c.length( ));
★代码与分析:
完整的程序如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MyHTTPSServerFile {
public static void main(String args[ ]) {
int i=0;
try {
SSLServerSocketFactory ssf= (SSLServerSocketFactory)
SSLServerSocketFactory.getDefault( );
ServerSocket ss=ssf.createServerSocket(443);
System.out.println("Web Server OK ");
while(true){
Socket s=ss.accept( ); //等待请求
PrintStream out = new PrintStream(s.getOutputStream( ));
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
String info=null;
String request=null;
while(( info=in.readLine())!=null){
if(info.indexOf("GET")!=-1){
//获取浏览器发来的get信息
request=info;
}
System.out.println("now got "+info);
if(info.equals("")) break;
}
System.out.println("now go");
System.out.println("now gotreq "+request);
if(request!=null){
out.println("HTTP/1.0 200 OK");
out.println("MIME_version:1.0");
out.println("Content_Type:text/html");
try{
// 浏览器请求形如 GET /t/1.html HTTP/1.1
// sp1, sp2为第一次和第二次出现空格的位置,
// filename从浏览器请求中提取出文件路径和名称 如 t/1.html
int sp1=request.indexOf(' ');
int sp2=request.indexOf(' ',sp1+1);
String filename=request.substring(sp1+2,sp2);
// 若浏览器请求中无文件名,则加上默认文件名index.html
if(filename.equals("") || filename.endsWith("/")){
filename+="index.html";
}
System.out.println("Sending "+filename);
// 向浏览器发送文件
File fi=new File(filename);
InputStream fs=new FileInputStream(fi);
int n=fs.available();
byte buf[]=new byte[1024];
out.println("Content_Length:"+n);
out.println("");
while ((n=fs.read(buf))>=0){
out.write(buf,0,n);
}
} catch(Exception e){
System.out.println(e);
}
out.close( );
s.close( );
in.close( );
} // end if
} // end while
} catch (IOException e) {
System.out.println(e);
}
}
}
作为测试的网页index.html内容如下:
<HTML>
<HEAD><TITLE>Test</TITLE></Head>
<body>
A test for HTTPS
Click <a href=test/1.bat> here </a> for a bat file <p>
</body>
</HTML>
它提供了一个指向当前目录test子目录1.bat文件的链接。。
★运行程序
本实例服务器程序工作在C:\java\ch7\Server目录,将如下命令在一行中输入批处理文件MyHTTPSServerFile.bat。
java -Djavax.net.ssl.keyStore=mycomstore2 -Djavax.net.ssl.keyStorePassword=wshr.ut MyHTTPSServerFile
运行MyHTTPSServerFile批处理文件,当显示“Web Server OK”后启动完成。
用户可在同一台计算机上按照7.3.1小节的方法在c:\windows目录hosts文件中加上如下一行:
127.0.0.1 www.my.com
也可以在另外一台计算机上按照7.3.1小节的方法在c:\windows目录hosts文件中加上如下一行:
202.120.1.1 www.my.com
其中202.120.1.1应该替换为运行本实例程序的计算机。则用户可以在浏览器中输入https://www.my.com访问该Web服务器,如图 7- 5所示。
单击其中的链接“here”,可以浏览到c:\java\ch7\server\test\1.bat文件的内容。
7.4基于证书的客户身份验证
7.4.1 最简单的验证客户身份的HTTPS服务器程序
★ 实例说明
本实例修改了7.3.1小节最简单的HTTPS服务器程序,使其支持客户身份的验证,它可以通过浏览器来访问,也可以通过本节后面的HTTPS客户程序来访问。
★ 编程思路:
程序和7.3.1小节类似,只是增加了一句执行ServerSocket对象的setNeedClientAuth(true)方法:
SSLServerSocket ss=(SSLServerSocket)ssf.createServerSocket(443);
ss.setNeedClientAuth(true);
★代码与分析:
完整代码如下:
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
public class MyHTTPSServerAuth {
public static void main(String args[ ]) {
int i=0;
try {
SSLServerSocketFactory ssf= (SSLServerSocketFactory)
SSLServerSocketFactory.getDefault( );
SSLServerSocket ss=
(SSLServerSocket)ssf.createServerSocket(443);
//要求客户验证
ss.setNeedClientAuth(true);
System.out.println("Web Server OK ");
while(true){
Socket s=ss.accept( ); //等待请求
PrintStream out = new PrintStream(s.getOutputStream( ));
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream( )));
String info=null;
while(( info=in.readLine())!=null){
System.out.println("now got "+info);
if(info.equals("")) break;
}
System.out.println("now go");
out.println("HTTP/1.0 200 OK");
out.println("MIME_version:1.0");
out.println("Content_Type:text/html");
i++;
String c="<html> <head></head><body> <h1> Hi, this is "
+i+"</h1></Body></html>";
out.println("Content_Length:"+c.length( ));
out.println("");
out.println(c);
out.close( );
s.close( );
in.close( );
}
} catch (IOException e) {
System.out.println(e);
}
}
}
★运行程序
和7.3.1小节一样,输入:
java -Djavax.net.ssl.keyStore=lfkeystore2 -Djavax.net.ssl.keyStorePassword=wshr.ut MyHTTPSServerAuth
或
java -Djavax.net.ssl.keyStore=mycomstore2 -Djavax.net.ssl.keyStorePassword=wshr.ut MyHTTPSServerAuth
运行程序,屏幕上出现“Web Server OK”,表明服务器已经正常启动。
由于本实例的服务器需要用户浏览器提供证书表明用户的身份,因此用户需首先在自己的计算机中安装代表用户的证书。可以如附录“申请数字标识(证书)”所示申请一个自己的证书,并如附录所示安装在自己的计算机中。
和7.3.1小节类似,在浏览器中输入https://127.0.0.1,在出现的图 7- 1所示的窗口中单击“确定”按钮。出现如图 7- 6所示窗口,该窗口中给出的是用户浏览器中已经安装的证书,选择证书后,单击“确定”按钮。出现图 7- 7所示的私钥容器提示信息,单击“确定”按钮后,如果HTTPS服务器信任用户浏览器提供的证书,则和7.3.1小节类似进入图 7- 8所示 的警告窗口。继续单击“确定”按钮,最终浏览到本小节服务器发送的信息。
在本实例中,由于用户计算机上安装的证书是按照附录“申请数字标识(证书)”中的步骤从Verisign申请得到的,Verisign的证书已经包括在Java默认的信任密钥库C:\j2sdk1.4.0\jre\lib\security\cacerts文件中了。因此本实例的服务器程序信任用户浏览器提交的数字证书。
除了通过默认信任密钥库设置服务器信任的密钥外,还可以通过Java命令选项-Djavax.net.ssl.trustStore进行设置。
7.4.2 编写客户程序连结需客户验证的HTTPS服务器
★ 实例说明
本实例使用7.3.2和7.3.3小节的程序代替浏览器访问7.4.1小节的需要客户验证的服务器程序。
★ 编程思路:
在7.3节中,运行服务器程序时指定了-Djavax.net.ssl.keyStore选项,服务器从中提取证书向客户程序表明自己是谁。而运行客户程序时指定了-Djavax.net.ssl.trustStore选项,指定客户信任哪些证书,这样当其接收到服务器程序发来的证书后就可以判断是否相信服务器。
在本节中,除了7.3节的验证外,客户程序也需要-Djavax.net.ssl.keyStore选项向服务器表明自己是谁,类似地,服务器程序应如7.4.1小节那样使用默认信任密钥库,或使用-Djavax.net.ssl.trustStore选项指定服务器信任谁。
本小节服务器不妨使用密钥库mycomstore2,该密钥库中my条目包含了证书www.my.com,服务器使用它来向用户宣称自己是谁。该证书是由CA "Xu Yingxiao"签发的。
服务器信任的密钥库使用clienttrust,其中包含了CA “Xu Yingxiao”的证书,这样,CA“Xu Yingxiao”签发的所有证书都被服务器程序信任。可将c:\java\ch7\client目录中clienttrust文件拷贝到c:\java\ch7\server目录。
客户程序不妨使用密钥库lfkeystore2,该密钥库中lf条目包含了“Liu Fang”的证书,客户程序使用它向服务器宣称自己是谁。该证书是由CA "Xu Yingxiao"签发的,由于服务器程序信任CA “Xu Yingxiao”签发的证书,因此将信任该证书。
客户程序使用的信任密钥库也使用clienttrust,即信任CA "Xu Yingxiao"签发的证书。
★代码与分析:
本实例服务器程序使用7.4.1小节的MyHTTPSServerAuth,客户程序使用7.3.2小节的HttpsClient和7.3.3小节的HttpsSocketClient程序。
★运行程序
本实例服务器程序工作在C:\java\ch7\Server目录,将如下命令在一行中输入批处理文件MyAuthServer.bat。
java -Djavax.net.ssl.keyStore=mycomstore2 -Djavax.net.ssl.keyStorePassword=wshr.ut -Djavax.net.ssl.trustStore=clienttrust MyHTTPSServerAuth
执行MyAuthServer,启动服务器程序,当屏幕提示“Web Server OK”后,服务器启动完毕。
本实例客户程序工作在C:\java\ch7\Client目录,使用7.3.2小节的程序HttpsClient,将如下命令在一行中输入批处理文件MyAuthClient.bat。
java -Djavax.net.ssl.keyStore=lfkeystore2 -Djavax.net.ssl.trustStore=clienttrust -Djavax.net.ssl.keyStorePassword=wshr.ut HttpsClient https://www.my.com
执行MyAuthClient,则客户程序开始运行,并显示服务器程序发来信息:
<html> <head></head><body> <h1> Hi, this is 1</h1></Body></html>
如果使用7.3.3小节的HttpsSocketClient,由于它在程序中已经指定了信任的密钥库clienttrust,因此不需要指定-Djavax.net.ssl.trustStore选项。只要在批处理文件MyAuthServer2.bat中输入如下一行命令:
java -Djavax.net.ssl.keyStore=lfkeystore2 -Djavax.net.ssl.keyStorePassword=wshr.ut HttpsSocketClient 127.0.0.1
执行后MyAuthServer2将显示:
HTTP/1.0 200 OK
MIME_version:1.0
Content_Type:text/html
Content_Length:65
<html> <head></head><body> <h1> Hi, this is 2</h1></Body></html>
本章介绍了基于SSL和HTTPS的数据加密传输,同时通过客户和服务器的数字证书,客户和服务器程序之间可以相互向对方表明自己的身份,实现程序之间的信任关系。
除了加密传输数据外,应用程序在运行过程中往往需要访问用户的本地资源,本书后面章节将介绍程序的安全运行。