11.3.3 代码验证

如果我们需要通过一个自己实现的客户端访问上述服务,该如何去做呢?我们是否需要了解SSL/TLS协议?我们是否需要完成相应的加密/解密操作,并对数据做验证/签名呢?

对于上述问题,Java语言的开发者已经将其封装。我们只需要获得数字证书,并将其导入密钥库应用相应的Java API,即可进行加密交互。

读者朋友需要先阅读第3章相关内容,了解SSL/TLS协议相关Java API。

这里我们直接使用本文中的密钥库文件zlex.keystore,对于加载密钥库密文件实现已在第10章中有详细描述,这里不再复述。

相信读者朋友对于HttpURLConnection这个类并不陌生,该类用于HTTP协议的访问。我们现在需要访问HTTPS协议,则需要HttpsURLConnection类。

HttpsURLConnection类与HttpURLConnection类差别不大,通过HttpsURLConnection类的setSSLSocketFactory()方法配置SSLSocektFactory实例就可将其作为一般HttpURLConnection类来操作。

SSLSocektFactory类用于管理SSL套接字,需要通过SSLContext类的getSocketFactory()方法获得。而SSLContext类初始化时则需要配置相应的密钥库与信任库。

在这里密钥库与信任库可以指向同一个文件,即zlex.keystore文件。我们可以将其加载配置SSLContext类的实例化对象。

获得密钥库管理工厂(KeyManagerFactory)如代码清单11-5所示。

代码清单11-5 获得密钥库管理工厂


//实例化密钥库管理工厂

KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());

//获得密钥库

KeyStore keyStore=getKeyStore(keyStorePath, password);

//初始化密钥库管理工厂

keyManagerFactory.init(keyStore, password.toCharArray());


上述getKeyStore()方法即加载密钥库方法,获得密钥库后就可初始化密钥库管理工厂。接下来,我们需要获得信任库管理工厂(TrustManagerFactory),如代码清单11-6所示。

代码清单11-6 获得信任库管理工厂


//实例化信任库管理工厂

TrustManagerFactory trustManagerFactory=TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

//获得信任库

KeyStore trustStore=getKeyStore(trustStorePath, password);

//初始化信任库管理工厂

trustManagerFactory.init(trustStore);


获得信任库实现与获得密钥库实现同出一辙,都是通过加载密钥库进行初始化操作。

完成上述操作后,我们就可以获得SSL上下文(SSLContext类),并获得SSL套接字工厂(SSLSocektFactory),如代码清单11-7所示。

代码清单11-7 获得SSL套接字工厂


//实例化SSL上下文

SSLContext ctx=SSLContext.getInstance("TLS");

//初始化SSL上下文

ctx.init(keyManagerFactory.getKeyManagers(),trustManagerFactory.getTrustManagers(),

new SecureRandom());

//获得SSLSocketFactory

SSLSocketFactory socketFactory=ctx.getSocketFactory();


这里,我们实例化SSL上下文时使用的是TLS协议,也可使用SSL协议。

获得SSLSocketFactory实例化对象后,我们只需要通过HttpsURLConnection类的setSSLSocketFactory()方法进行配置即可。完整代码如代码清单11-8所示。

代码清单11-8 HttpsURLConnection配置


import java.io.FileInputStream;

import java.security.KeyStore;

import java.security.SecureRandom;

import javax.net.ssl.HttpsURLConnection;

import javax.net.ssl.KeyManagerFactory;

import javax.net.ssl.SSLContext;

import javax.net.ssl.SSLSocketFactory;

import javax.net.ssl.TrustManagerFactory;

/**

*HTTPS组件

*@author梁栋

*@version 1.0

*/

public abstract class HTTPSCoder{

/**

*协议

*支持TLS和SSL协议

*/

public static final String PROTOCOL="TLS";

/**

*获得KeyStore

*@param keyStorePath密钥库路径

*@param password密码

*@return KeyStore密钥库

*@throws Exception

*/

private static KeyStore getKeyStore(String keyStorePath, String password)

throws Exception{

//实例化密钥库

KeyStore ks=KeyStore.getInstance(KeyStore.getDefaultType());

//获得密钥库文件流

FileInputStream is=new FileInputStream(keyStorePath);

//加载密钥库

ks.load(is, password.toCharArray());

//关闭密钥库文件流

is.close();

return ks;

}

/**

*获得SSLSocektFactory

*@param password密码

*@param keyStorePath密钥库路径

*@param trustStorePath信任库路径

*@return SSLSocketFactory

*@throws Exception

*/

private static SSLSocketFactory getSSLSocketFactory(String password, String

keyStorePath, String trustStorePath)throws Exception{

//实例化密钥库

KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance

(KeyManagerFactory.getDefaultAlgorithm());

//获得密钥库

KeyStore keyStore=getKeyStore(keyStorePath, password);

//初始化密钥工厂

keyManagerFactory.init(keyStore, password.toCharArray());

//实例化信任库

TrustManagerFactory trustManagerFactory=

TrustManagerFactory.getInstance(TrustManagerFactory.

getDefaultAlgorithm());

//获得信任库

KeyStore trustStore=getKeyStore(trustStorePath, password);

//初始化信任库

trustManagerFactory.init(trustStore);

//实例化SSL上下文

SSLContext ctx=SSLContext.getInstance(PROTOCOL);

//初始化SSL上下文

ctx.init(keyManagerFactory.getKeyManagers(),trustManagerFactory.

getTrustManagers(),new SecureRandom());

//获得SSLSocketFactory

return ctx.getSocketFactory();

}

/**

*为HttpsURLConnection配置SSLSocketFactory

*@param conn HttpsURLConnection

*@param password密码

*@param keyStorePath密钥库路径

*@param trustStorePath信任库路径

*@throws Exception

*/

public static void configSSLSocketFactory(HttpsURLConnection conn, String

password, String keyStorePath, String trustStorePath)throws Exception{

//获得SSLSocketFactory

SSLSocketFactory sslSocketFactory=getSSLSocketFactory(password,

keyStorePath, trustStorePath);

//设置SSLSocketFactory

conn.setSSLSocketFactory(sslSocketFactory);

}

}


如果HTTPS连接建立失败会怎样?我们将不能从连接中获得有效的ContentLength,即ContentLength值为-1。根据这一特性,我们可以鉴别HTTPS连接是否成功,并验证是否可以获得内容,相关实现如代码清单11-9所示。

代码清单11-9 获取HTTPS连接内容


//鉴别内容长度

int length=conn.getContentLength();

byte[]data=null;

//如果内容长度为-1,则放弃解析

if(length!=-1){

DataInputStream dis=new DataInputStream(conn.getInputStream());

data=new byte[length];

dis.readFully(data);

dis.close();

System.err.println(new String(data));

}

//关闭连接

conn.disconnect();


如果HTTPS连接成功,我们可以供输入流中获得相应的信息,并且该信息已经过解密,我们可以在控制台的到相应的内容。

完整的测试用例如代码清单11-10所示。

代码清单11-10 HTTPS连接测试


import static org.junit.Assert.*;

import java.io.DataInputStream;

import java.net.URL;

import javax.net.ssl.HttpsURLConnection;

import org.junit.Test;

/**

*HTTPS测试

*@author梁栋

*@version 1.0

*/

public class HTTPSCoderTest{

//密钥库/信任库密码

private String password="123456";

//密钥库文件路径

private String keyStorePath="d:/zlex.keystore";

//信任库文件路径

private String trustStorePath="d:/zlex.keystore";

//访问地址

private String httpsUrl="https://www.zlex.org/ssl/";

/**

*HTTPS验证

*@throws Exception

*/

@Test

public void test()throws Exception{

//建立HTTPS链接

URL url=new URL(httpsUrl);

HttpsURLConnection conn=(HttpsURLConnection)url.openConnection();

//打开输入输出流

conn.setDoInput(true);

//为HttpsURLConnection配置SSLSocketFactory

HTTPSCoder.configSSLSocketFactory(conn, password, keyStorePath, trustStorePath);

//鉴别内容长度

int length=conn.getContentLength();

byte[]data=null;

//如果内容长度为-1,则放弃解析

if(length!=-1){

DataInputStream dis=new DataInputStream(conn.getInputStream());

data=new byte[length];

dis.readFully(data);

dis.close();

System.err.println(new String(data));

}

//关闭连接

conn.disconnect();

//验证

assertNotNull(data);

}

}


通过代码访问https://www.zlex.org/ssl/页面,我们在控制台得到request相关属性与通过浏览器访问获得的内容有所不同。除了javax.servlet.request.ssl_session属性必然发生变化外,javax.servlet.request.cipher_suite属性也发生了变化。这里javax.servlet.request.cipher_suite属性值为“SSL_RSA_WITH_RC4_128_MD5”,而非“TLS_RSA_WITH_AES_128_CBC_SHA”。这说明不同的客户端在同一时间访问同一服务时,将有可能使用不同的协议或算法。

在上述代码中,我们并没有做过任何加密/解密操作。我们像使用HttpURLConnection类一样使用HttpsURLConnection类构建连接,我们无需关心加密/解密的具体实现。这些加密/解密的实现层位于传输层,对于应用层完全透明,这极大地方便了我们的使用。