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类构建连接,我们无需关心加密/解密的具体实现。这些加密/解密的实现层位于传输层,对于应用层完全透明,这极大地方便了我们的使用。