Java编程语言的一个杰出之处就在于开源社区可以以较低的成本或者甚至是免费地提供优秀的应用程序。其中一个例子就是Apache Tomcat,它为使用servlet或JSP技术的开发提供了一个健壮的Web服务器。现在Web服务技术正日趋成熟,所以有些应用程序就有可能利用Swing特性丰富的前端瘦客户端结合Web或ejb层已经开发出来的数据验证和业务逻辑。此类应用程序只有在受到保护的情况下才能正常运行,不过,安全性不一定意味着昂贵的成本。本文的目的就是要演示Web服务客户端如何通过安全的HTTPS协议使用自签名的安全证书。
使用自签名证书的问题
HTTPS通常可以无缝地与不安全的HTTP协议一起使用,而不中断用户的体验。这是由于SSL被设计为由可信的第三方进行验证和签名。Verisign是一家流行的认证机构。如果您的Web应用程序要求安全的通信,那么您就可以付钱给Verisign来签名您的SSL证书。经过Verisign签名之后,您的Web站点上的用户就可以不中断地在HTTP与HTTPS之间进行切换,因为所有主流Web浏览器都信任由Verisign签名的证书。但是Verisign并不是获得签名证书的惟一选择。为了节省运作成本,或者为了个人使用方便,您也可以自签名自己的证书。但是,自签名证书会中断Web站点用户的体验。通常Web浏览器会显示一个对话框,询问您是否希望信任一个自签名证书。
Web浏览器的这一特性很好,因为当其获得一个由未知认证机构签名的证书时,还有机会进行处理。在开发用于通过HTTPS进行通信的Web服务客户端时,这就没那么容易了。在运行Java代码时,不会出现询问是否信任一个不可信的认证机构的对话框。JRE会抛出一个异常,说明试图通过HTTPS连接到一个具有不可信证书的Web站点:
Caused by:sun.security.validator.ValidatorException:No trusted certificate found
无法捕获此异常并继续。要让Web服务使用自签名证书,JRE必须以某种方式将您当作认证机构信任。
解决方案概述
为演示此问题的解决方案,我将执行以下步骤:
- 生成并自签名我自己的证书;
- 为Tomcat配置SSL,使其使用该证书;
- 创建一个示例Web服务,以便通过HTTPS调用;
- 从WSDL生成Web服务客户端代码;
- 使用定制的密钥库解决方案演示客户端;
生成自签名证书
JDK附带了一个工具,keytool.exe,用于管理SSL公钥/私钥。密钥在文件系统的一个二进制文件中进行添加和删除。默认的密钥库文件是JAVA_HOME/jre/lib/security/cacerts。该文件包含了JRE所信任的认证机构的列表。一个知名可信公司(比如Verisign)的列表已经存在于密钥库中了。要查看该列表,可使用口令changeit执行以下代码:
D:/>keytool -list -rfc -keystore JAVA_HOME/jre/lib/security/cacerts
Keytool应用程序可用于编辑此文件。但是,为了防止出错,最好还是创建一个新文件。如果不告知keytool使用哪个文件,它就会默认地创建HOME/.keystore。
要生成自己的自签名证书,可执行:
D:/>keytool.exe -genkey -alias Tomcat -keyalg RSA -storepass bigsecret -keypass bigsecret -dname "cn=localhost"
执行完该命令后,就会在HOME目录下生成一个.keystore文件。下面是各种切换命令的含义:
- genkey:告诉keytool应用程序生成新的公钥/私钥对。
- alias:用于引用密钥的名称。记住,.keystore文件可包含多个密钥。
- Keyalg:使用RSA算法生成公钥/私钥对。
- Storepass:访问.keystore文件所需的口令。
- Keypass:管理密钥所需的口令。
- dname:该值非常重要。.我使用了localhost,因为该示例被设计为本地运行。如果一个Web应用程序被注册为http://www.myserver.com,那么该值就必须是www.myserver.com。如果名称不匹配,证书就会自动被拒绝。
一旦keytool应用程序创建了一个新的公钥/私钥对,它就自动自签名该密钥。我们刚刚生成了自己的自签名证书,它可用于HTTPS通信。只需提取出自签名公钥。后面我将展示如何做。
为Tomcat配置SSL
现在必须配置Tomcat,使其使用自签名证书。我使用的是Tomcat 5.0.30。编辑TOMCAT/conf/server.xml文件。在文件中搜索“8443”,并取消绑定到该端口的<Connector.../>注释。然后必须向<Connector.../>添加下属属性:
keystorePass="bigsecret"
当JRE启动时,它将自动找到HOME/.keystore文件,并且Tomcat会试着使用口令“bigsecret”访问它。在Tomcat启动时,控制台应该有如下输出:
Feb 4, 2006 3:11:23 PM org.apache.coyote.http11.Http11Protocol start
INFO:Starting Coyote HTTP/1.1 on http-8443
这意味着<Connector.../>成功地读取了.keystore文件,现在可以通过8443端口进行安全的HTTPS连接了。打开Web浏览器,并在地址栏输入https://localhost:8443/。因为该证书是自签名的,所以Web浏览器将显示一个对话框,询问是否信任该连接。如果接受,则所有的通信都将通过HTTPS进行,从而成为安全的。
创建Web服务
我将使用Apache Axis项目创建一个非常简单的Web服务。该Web服务将模拟检查新的电子邮件消息。Web服务客户端传递一个惟一地识别一个用户的令牌。Web服务返回一个新电子邮件消息的列表(参见清单1)。
清单1
<span style="font-family:宋体;font-size:12px;">import java.util.*; public class Email { public List getNewMessages(String id) { List l = new ArrayList(3); l.add("1"); l.add("2"); l.add("3"); return l; } } </span>
要获取已部署的Web服务,执行以下步骤:
- 从清单1剪切并粘贴代码到Webapp的根目录下的Email.jws文件。
- 编辑Web.xml文件,添加Axis servlet以及一个*.jws映射(清单2)。
- 将Axis jar文件放入WEB-INF/lib。请参见文章末尾的“参考资料”部分,获取Axis项目URL。
清单2
<span style="font-family:宋体;font-size:12px;"><servlet> <servlet-name> AxisServlet </servlet-name> <display-name> Apache-Axis Servlet </display-name> <servlet-class> org.apache.axis.transport.http.AxisServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name> AxisServlet </servlet-name> <url-pattern> *.jws </url-pattern> </servlet-mapping></span>
在部署了本文所附带的WAR文件(并为Tomcat配置SSL)之后,Web就可以安全地通过HTTPS使用下面的URL来访问了:
https://localhost:8443/JDJArticleWebService/Email.jws
使用WSDL2Java
Axis项目提供了一个名为WSDL2Java的工具,它获取一个Web服务WSDL并自动创建使用该Web服务所需的Java源代码。参见清单3中用于生成Email.jws Web服务代码的命令行。
清单3
<span style="font-family:宋体;font-size:12px;">java -classpath .;axis.jar;log4j-1.2.8.jar ;commons-logging-1.0.4.jar ;commons-discovery-0.2.jar ;jaxrpc.jar;saaj.jar ;wsdl4j-1.5.1.jar org.apache.axis.wsdl.WSDL2Java -p jdj.wsclient.shared http://localhost:8080/JDJArticle/Email.jws?wsdl </span>
注意清单3中用于访问WSDL的URL。它在8080端口使用了不安全的HTTP协议。为什么不在8443端口使用HTTPS呢?这是因为自签名证书,WSDL2Java工具将遇到与本文所试图解决的证书问题完全相同的问题。所以现在必须使用使用不安全的协议。这意味着生成的代码必须有一点改变,使用“https”和“8443”替换“http”和“8080”引用。本文所附带的客户端zip文件包含了更改后的代码。
具有定制密钥库的客户端
JRE的默认密钥库是JAVA_HOME/jre/lib/security/cacerts。只要出现自签名证书,Java应用程序就会抛出异常,因为该证书不在密钥库中。因此,在开发客户端时有两种选择。第一种选择是将自签名证书放入该JRE的默认密钥库中。虽然这种方法有效,但是它并不是一个好的解决方案,因为需要在每个客户端机器上进行定制化。第二种选择是生成一个定制的密钥库,将自签名证书放入其中,并将定制密钥库作为应用程序的一部分分发(通常在一个jar文件中)。
要为客户端创建定制密钥库,需要执行以下步骤:
- 从HOME/.keystore导出自签名公钥。
- 将自签名公钥导入到为客户端创建的新密钥库中。
要从HOME/.keystore导出自签名公钥,可执行以下代码:
D:/>keytool.exe -genkey -alias Tomcat -keyalg RSA -storepass bigsecret -keypass bigsecret -dname "cn=localhost"
现在通过导入Tomcat.cer,为客户端创建定制密钥库:
D:>keytool.exe -import -noprompt -trustcacerts -alias Tomcat -file Tomcat.cer -keystore CustomKeystore -storepass littlesecret
使用“-keystore CustomKeystore”,将会在当前工作目录中创建一个名为CustomKeystore的新密钥库文件。可以在本文的客户端zip文件的/classpath/resources/keystore目录下找到CustomKeystore文件。使用刚刚生成的文件替换该文件。
现在只剩下创建一个使用该定制密钥库的客户端了。.我将演示两种实现方法。
第一种方法是使用Java系统属性javax.net.ssl.trustStore和javax.net.ssl.trustStorePassword来指向CustomKeystore文件,并提供访问该文件的口令。jdj.wsclient.truststore包中的示例Web服务客户端使用的就是这种方法(参见清单4)。
清单4
<span style="font-family:宋体;font-size:12px;">public static void main(String[] args) throws Exception { System.setProperty( "javax.net.ssl.trustStore", "classpath/resources/keystore/CustomKeystore"); System.setProperty( "javax.net.ssl.trustStorePassword", "littlesecret"); EmailServiceLocator wsl = new EmailServiceLocator(); Email_PortType ews = wsl.getEmail(); Object [] objects = ews.getNewMessages("12345"); out("Msg Count: " + objects.length); } </span>
main()方法设置系统属性,然后创建使用该Web服务的对象。当JRE需要访问密钥库时,它就在文件系统中寻找classpath/resources/keystore/CustomKeystore文件。虽然这只是一个简单的解决方案,但它还是存在问题,因为密钥库文件必须放在文件系统中,而客户端代码也必须知道在哪里找到它。
第二种解决方案具有更好的可移植性,它将资源放在jar文件中,从而避免了文件系统问题。客户端代码负责读取CustomKeystore文件,并以某种方式使用它创建到服务器的安全连接。jdj.wsclient.socketfactory包中的示例Web服务客户端使用的就是这种方法(参见清单5)。
清单5
<span style="font-family:宋体;font-size:12px;">public MySocketFactory(Hashtable table) throws Exception { out("Created!"); KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() ); char [] password = "littlesecret".toCharArray(); String keystore = "/resources/keystore/CustomKeystore"; Class tclass = this.getClass(); InputStream is = tclass.getResourceAsStream( keystore ); ks.load(is, password); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks,password); TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); tmf.init(ks); SSLContext context = SSLContext.getInstance("SSL"); context.init( kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom() ); factory = context.getSocketFactory(); } </span>
清单5显示了如何将CustomKeystore文件作为资源读取,并使用它来创建javax.net.ssl.SSLSocketFactory。配置Axis可插入架构,然后可使用MySocketFactory类从该工厂创建安全的Socket对象。
结束语
本文以一个简单的问题开始:我希望使用自签名的证书保护通过HTTPS的Web服务通信。默认情况下,JRE会拒绝应用程序的自签名证书,因为它不是来自于可信的认证机构。要让安全的通信可运行,必须让Web服务客户端JRE信任自签名证书。为此,我使用keytool应用程序生成一个新的公钥/私钥对,提取出自签名公钥,然后创建一个新的密钥库,并导入该自签名证书。然后我创建一个不需要任何客户端配置的、完全自包含的Web服务客户端。