Java Tip 96: 使用 HTTPS 编写客户端程序
如何在标准 URL 类中使用 HTTPS 协议
By Matt Towers
摘要
使用 HTTPS(Hypertext Transfer Protocol Secure 安全超文本传输协议)并非你所想的那样简单直接。如果你曾经尝试在 Java 客户端和 HTTPS 服务器之间进行安全的通讯,也许会注意到标准的 java.net.URL 类并不支持 HTTPS协议。这篇文章将向你展示,如何使用 JDK 1.2-compatible 虚拟机或微软的 JDK 1.1-compatible JView 来克服这些限制。
如果你曾经尝试在 Java 客户机和 HTTPS(安全超文本传输协议)服务器之间进行安全的通讯,也许会注意到标准的
java.net.URL
类并不支持 HTTPS 协议。服务端解决此问题的方法是非常简单明了的。因为现今几乎所有的Web服务器都使用 HTTPS 协议来提供查询数据的机制。一旦配置好你的服务器,任何浏览器只要简单地将 URL 地址中的协议指定成 HTTPS ,就能够在你的服务器上安全地进行信息查询。如果你没有搭建起 HTTPS 服务器,则可以在互联网上几乎所有 HTTPS 网页中测试你的客户端代码。在 资料部分给出了一个列表,里面列出若干可供你进行 HTTPS 通讯测试的服务器地址。
然而从客户端的角度来看,在熟悉的 HTTP 后面简单的加上“S”就能够安全通信。这种简单性充满了迷惑性。事实上,浏览器在后台做了大量的工作,以保证没有任何人篡改或窃听你所发送的请求数据。然而 HTTPS 协议用来加密的算法是 RSA Security 所拥有的专利(这种状况至少还要持续几个月)。该加密算法得到了浏览器制造商的许可,但 Sum Microsystems 公司却不同意将它绑定到标准的
Java URL
类实现中。这就导致当你创建 URL 对象时,若将协议指定为 HTTPS,就会抛出一个
MalformedURLException
异常。
幸运的是,为了解决这个局限,Java规格说明书提供为
URL
类选择一个代替的流句柄的能力。然而当你使用不同的虚拟机( virtual machine )时,此技术的实现方法也是不同的。在微软的 JDK 1.1-compatible 虚拟机 JView 中,微软许可该加密算法并提供了一个 HTTPS 流句柄作为它的
wininet
包的一部分。而SUN最近为它的 JDK 1.2-compatible 虚拟机发布了 Java Secure Sockets Extension(JSSE),在 JSSE 里许可并提供了 HTTPS 流句柄。本文将具体阐述如何使用 JSSE 和微软的
wininet
包来实现 HTTPS 流句柄。
JDK 1.2-compatible 虚拟机
在 JDK 1.2-compatible 虚拟机中使用 HTTPS 的技术主要依赖于 Java Secure Sockets Extension(JSSE)1.0.1 版本。你必须先安装 JSSE 并且将它添加到客户端虚拟机的类路径中,才能够使用这项技术。
当你安装好 JSSE 之后,你必须设置一项系统属性,将一个新的安全提供者添加到
Security
类对象。完成这项要求有若干种方法。鉴于这篇文章的目的,在这里介绍一种实用方法:
System.setProperty("java.protocol.handler.pkgs",
"com.sun.net.ssl.internal.www.protocol");
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
完成以上两个函数调用之后,运行如下所示代码时将不再抛出
MalformedURLException
异常:
URL url = new URL("https://[your server]");
如果连接标准的SSL端口443,可以忽略在URL字符串的后面添加端口号这一选项。但倘若你所连接的的Web服务器采用非标准端口进行SSL通讯,就需要像下面这样,在URL字符串后面添加该端口号:
URL url = new URL("https://[your server]:7002");
有些服务器拥有的可能是无签名或非法的 SSL 证明书。倘若涉及这类服务器的URL,使用此方法就需要注意。这种情况下,如果试图从URL的连接对象中检索输入或输出流时(例如运行以下代码),就会抛出一个
SSLException
异常,并显示 "untrusted server cer chain." 消息。如果该服务器拥有合法且有签名的证明书,则不会抛出任何异常。
URL url = new URL("https://[your server]");
URLConnection con = URL.openConnection();
//SSLException thrown here if server certificate is invalid
con.getInputStream();
最显而易见的解决上述问题的方法就是为你的服务器取得一个签名证明书。而 Java Developer Connectin forums 中为此问题成立了一个工作区,可以在这个URL地址中找到它们的相关信息:http://forum.java.sun.com/[email protected]@.787ad8de.
Microsoft JView
由于微软和Sun公司关于在 Windows 平台上使用 Java 的许可问题有争论,微软的 JView 虚拟机现在仅仅是基于 JDK 1.1-compliant 的。而 JSSE 需要 1.2.2-compatible 或以上版本的虚拟机,因此上述的方法并不适用于在 JView 上运行的客户机。但是微软同样也提供了足够方便的 HTTPS 流句柄,将其作为
com.ms.net.wininet
包的一部分。
在 JView 环境中,只要为
URL
类调用一个简单静态函数就能够设置 HTTPS 流句柄:
URL.setURLStreamHandlerFactory(new
com.ms.net.wininet.WininetStreamHandlerFactory());
执行以上的函数调用之后,再运行下面的代码就不会抛出
MalformedURLException
异常:
URL url = new URL("https://[your server]");
使用这种方法需要注意两点。首先,根据JDK的文档,
serURLStreamHandlerFactory
函数在一个虚拟机上最多只能被调用一次。之后的调用将会产生
Error
。其次,正如在1.2虚拟机解决方案中所说,使用那些指向无签名或非法证明书的服务器的 URL 时必须要谨慎。同前所述,这种情况下试图向该 URL 地址的连接对象检索输入或输出数据流时,就会出问题。不过微软的流句柄抛出的是一个标准
IOExceptiony
异常,而不是
SSLException
。
URL url = new URL("https://[your server]");
URLConnection con = url.openConnection();
//IOException thrown here if server certificate is invalid
con.getInputStream();
同样,解决此问题最显而易见的方法就是仅和那些拥有签名和合法证明书的服务器进行通讯。不过JView还提供了另外一个选择。将要向 URL 连接目标检索输入输出流之前,你可以先为 connection 对象调用
setAllowUserInteraction(true)
函数。JView 在运行时就会显示消息,向用户警告该服务器的证明书是非法的,用户可以选择是否继续。始终要记住的是,这样的消息在桌面应用程序中是合乎情理的,但是除了调试的目的外,让你的服务器任何情况下都弹出消息框可能是不可取的。
注意:你也可以在 JDK 1.2-compatible 版本的虚拟机中调用
setAllowUserInteraction()
函数。不过,在Sun的1.2虚拟机上(测试以下代码),即使将该函数的参数设成true,也不会显示消息框。
URL url = new URL("https://[your server]");
URLConnection con = url.openConnection();
//causes the VM to display a dialog when connecting
//to untrusted servers
con.setAllowUserInteraction(true);
con.getInputStream();
在 Windows NT4.0 ,Windows2000 和 Windows9x 操作系统中,
com.ms.net.wininet
包被缺省安装到系统的类路径下。此外,根据微软的JDK文档,
WinInetStreamHandlerFactory
是"… the same handler that is installed by default when running applets.",即运行applet时,同样的流句柄也会被缺省安装。
平台独立性
尽管上述的两种方法覆盖了大部分Java客户程序可能运行的平台,你的Java客户程序也许需要在 JDK 1.1 和 JDK 1.2-compliant 虚拟机上都可以正确运行。"写一次,在任何地方运行,"还记得吗?很自然会想到将这两种方法结合起来,根据运行的虚拟机执行相应的处理句柄。下面的代码展示了一种达到此目的的方法:
String strVendor = System.getProperty("java.vendor");
String strVersion = System.getProperty("java.version");
//Assumes a system version string of the form:
//[major].[minor].[release] (eg. 1.2.2)
Double dVersion = new Double(strVersion.substring(0, 3));
//If we are running in a MS environment, use the MS stream handler.
if( -1 < strVendor.indexOf("Microsoft") )
{
try
{
Class clsFactory =
Class.forName("com.ms.net.wininet.WininetStreamHandlerFactory" );
if ( null != clsFactory )
URL.setURLStreamHandlerFactory(
(URLStreamHandlerFactory)clsFactory.newInstance());
}
catch( ClassNotFoundException cfe )
{
throw new Exception("Unable to load the Microsoft SSL " +
"stream handler. Check classpath." + cfe.toString());
}
//If the stream handler factory has
//already been successfully set
//make sure our flag is set and eat the error
catch( Error err ){m_bStreamHandlerSet = true;}
}
//If we are in a normal Java environment,
//try to use the JSSE handler.
//NOTE: JSSE requires 1.2 or better
else if( 1.2 <= dVersion.doubleValue() )
{
System.setProperty("java.protocol.handler.pkgs",
"com.sun.net.ssl.internal.www.protocol");
try
{
//if we have the JSSE provider available,
//and it has not already been
//set, add it as a new provide to the Security class.
Class clsFactory = Class.forName("com.sun.net.ssl.internal.ssl.Provider");
if( (null != clsFactory) && (null == Security.getProvider("SunJSSE")) )
Security.addProvider((Provider)clsFactory.newInstance());
}
catch( ClassNotFoundException cfe )
{
throw new Exception("Unable to load the JSSE SSL stream handler." +
"Check classpath." + cfe.toString());
}
}
关于applets
在 applet 中进行基于 HTTPS 的通讯,看起来似乎是上述内容的自然扩展。事实上,在大多数情况下applet中的HTTPS通讯更易于实现。在 Netscape Navigator 和 Internet Explorer 的4.0或更高版本中,它们各自的虚拟机都缺省许可HTTPS协议。因此,倘若你要在applet代码中创建一个HTTPS连接,只要在创建
URL
实例时将协议名称指定为"HTTPS"便可。
URL url = new URL("https://[your server]");
如果客户端浏览器运行的是Sun公司的Java 2插件,那么当你使用 HTTPS 时还会遇到一些其他限制。关于在Java 2插件中使用 HTTPS 的详细讨论可以在Sun公司站点上找到(参看本文末的资料)。
结论
在应用程序中使用 HTTPS 协议,是一种快速而高效地在通讯中获得足够的安全性的方法。不幸的是,更多地出于法律而不是技术方面的原因,它没有被标准 Java 规格说明书所支持。无论如何,随着 JSSE 的产生以及微软
com.ms.net.wininet
包的使用,在大多数的平台上只需要少许几行代码就能够实现安全通讯。
关于作者 Matt Towers, 自称为eBozo,最近离开了他在Visio的职位。此后加入华盛顿西雅图的一个互联网公司PredictPoint.com ,在那里从事全职的Java开发工作。 |
资料
- 在本文中所描述的跨平台实现代码,是在一个叫做
类中实现的。HttpsMessage
是HttpsMessage
类的子类。HttpMessage
类的作者是Jason Hunter,即Java Servlet Programming(O'Reilly & Associates) 一书的作者. 在他即将出版的该书第二版中,你可以找到HttpMessage
类。如果想要继承此类,必须下载并安装HttpsMessage
com.oreily.servlets
包。这个包以及相关子源代码可以在Hunter的站点上找到:
http://www.servlets.com
- 你也可以下载
HttpsMessage
类源码的压缩文件:
HttpsMessage.zip
- 以下是若干用于测试HTTPS通讯的网页地址:
- https://www.verisign.com/
- https://happiness.dhs.org/
- https://www.microsoft.com
- https://www.sun.com
- https://www.ftc.gov
-
更多关于JSSE、可下载位和安装指令的信息可以在Sun公司的站点上找到:
http://java.sun.com/products/jsse/.
-
关于如何使用一些JSSE服务的描述,包括上文所提到的方法,都可以在O'Reilly网站上Jonathan Knudsen 所编写的"Secure Networking in Java"中找到:
http://java.oreilly.com/bite-size/java_1099.html
- 更多关于
WininetStreamHandlerFactory
类的信息可以在微软的JSDK文档中找到:http://www.microsoft.com/java/sdk/。此外,Microsoft knowledge base还出版了"PRB: Allowing the URL class to access HTTPS in Applications":
http://support.microsoft.com/support/kb/articles/Q191/1/20.ASP
-
关于在Java 2插件中使用HTTPS的更多信息可以在Sun站点的"How HTTPS Works in Java Plug-In "中找到:
http://java.sun.com/products/plugin/1.2/docs/https.html