JSSE(Java Security Sock 通讯而推出的解决方案。它实现 消息完整性和客户端验证等技术 地传输数据。这篇文章主要描述 | et Extension,Java安全套接字扩展 了SSL和TSL(传输层安全)协议。在 。通过使用JSSE,开发人员可以在客 如何使用JSSE接口来控制SSL连接。 | )是Sun为了解决在Internet上的安全 JSSE中包含了数据加密,服务器验证, 户机和服务器之间通过TCP/IP协议安全 |
首先我通过一个简单的客户 配置KeyStore和TrustStore文件 论授权和身份验证方面的问题。 | 机/服务器程序来介绍如何利用JSSE ,这样在程序中我们才可以从客户端 通过从KeyStore中选择不同的授权, | 进行编程。当建立客户端时,我们需要 的文件系统中加载它们。然后文章将讨 客户端程序可以连接到不同的服务器。 |
运行例子程序 |
下载例子程序 |
在运行JSSE程序前,你需要 如果你使用的是其他版本的Java JSSE是在J2SE 1.4中才成为标准 子都是在J2SE 1.4下调试的,因 | 正确安装JSSE。如果你安装了J2SE 1 ,你需要从官方站点上下载并安装JS 的,并且J2SE 1.4中的JSSE和以前的 此推荐你使用J2SE 1.4运行这些例子 | .4,JSSE已经被自动安装并配置好了, SE,安装过程这里就不再赘述。由于 JSSE有一些细微的差别,而且文中的例 。 |
在深入介绍JSSE之前,让我们来一个简单的客 SimpleSSLServer和SimpleSSLClient。在运行程序 | 户机/服务器程序,程序中包含了两个文件: 之前,你需要配置下面这些KeyStore和TrestStore文件: |
· 一个客户端的KeyStore文件,该文件中包含了对Alice和Bob的授权。 |
· 一个服务器端的KeyStore文件,该文件中包含了对server的授权。 |
· 一个名为clientTrust的客户端TrustStore | 文件,该文件中包含了对server的授权。 |
· 一个名为serverTrust的 | 服务器端TrustStore文件,该文件中 | 包含了对Alice和Bob的授权。 |
使用keytool可以帮助你创建这些文件(该工具在Java的bin目录下): |
· 一个客户端的KeyStore文件,该文件中包含了对Alice和Bob的授权。 |
在命令窗口中输入下面的命令: |
keytool -genkey -alias alice -keystore c | lientKeys |
窗口中会出现下面的提示,根据提示输入相应的信息: |
输入keystore密码: password |
您的名字与姓氏是什么? |
[Unknown]: Alice |
您的组织单位名称是什么? |
[Unknown]: Development |
您的组织名称是什么? |
[Unknown]: DCQ |
您所在的城市或区域名称是什么? |
[Unknown]: ChongQing |
您所在的州或省份名称是什么? |
[Unknown]: ChongQing |
该单位的两字母国家代码是什么 |
[Unknown]: CH |
CN=Alice, OU=Development | , O=DCQ, L=ChongQing, ST=ChongQi | ng, C=CH 正确吗? |
[否]: 是 |
输入的主密码 |
(如果和 keystore 密码相同,按回车): |
通过相同的方式可以建立对Bob的授权。 |
keytool -genkey -alias b | ob -keystore clientKeys |
注意在名字与姓氏一栏中填 | 写Bob。在完成后可以键入下面的命 | 令来检测是否已经正确完成了授权。 |
keytool -list -v -keystore clientKeys |
· 一个服务器端的KeyStore文件,该文件中包含了对server的授权。 |
在命令窗口中键入下面的命令: |
keytool -genkey -alias server -keystore | serverKeys |
注意将密码设为password,名字与姓氏设定为 。 | Server。完成授权后同样可以通过上面提到的命令来检测 |
· 一个名为clientTrust的 serverTrust的服务器端TrustSt | 客户端TrustStore文件,该文件中包 ore文件,该文件中包含了对Alice和 | 含了对server的授权。以及一个名为 Bob的授权。 |
keytool -export -alias s | erver -keystore clientKeys -file | server.cer |
输入keystore密码: password |
保存在文件中的认证 |
keytool -export -alias alice -keystore c | lientKeys -file alice.cer |
输入keystore密码: password |
保存在文件中的认证 |
keytool -export -alias b | ob -keystore clientKeys -file bo | b.cer |
输入keystore密码: password |
保存在文件中的认证 |
这样keytool就在当前目录 件中;将alice.cer和bob.cer导 | 下创建了三个授权文件。然后我们将 入到serverTruest文件中: | server.cer文件导入到clientTrust文 |
keytool -import -alias server -keystore | clientTrust -file server.cer |
keytool -import -alias a | lice -keystore serverTrust -file | alice.cer |
keytool -import -alias bob -keystore ser | verTrust -file bob.cer |
到目前为止,在当前目录下 了KeyStore和TrustStore的设置 | 包含clientKeys,serverKeys,clie 后就可以运行例子程序了。首先需要 | ntTrust,serverTrust四个文件。完成 运行服务器程序: |
java -Djavax.net.ssl.keyStore=serverKeys |
-Djavax.net.ssl.ke | yStorePassword=password |
-Djavax.net.ssl.trustStore=serverTrust |
-Djavax.net.ssl.trustStorePassword=pas | sword SimpleSSLServer |
在命令行中我们指定了keyS 指定trustStore为serverTrust 当服务器程序成功运行后,你会 | tore属性为serverKeys。由于服务器 。这样SSLSimpleServer就可以验证 看到下面的提示: | 程序需要获得客户端的授权信息,我们 由SSLSimpleClient提供的授权信息。 |
SimpleSSLServer running on port 49152 |
这时候服务器会等待客户端发出建立连接的申 命令中指定-port xxx参数,其中xxx是端口号。 | 请。如果你希望在另一个端口上运行服务器程序,可以在 |
然后在另一个命令窗口中运行客户端程序: |
java -Djavax.net.ssl.keyStore=clientKeys |
-Djavax.net.ssl.keyStorePassword=password |
-Djavax.net.ssl.trustStore=clientTrust |
-Djavax.net.ssl.trustStorePassword=pas | sword SimpleSSLClient |
客户端程序会试图向本机的 过-host参数指定主机名称。当 | 49152端口建立SSL连接。同样你可以 连接成功后,会出现下面的提示信息 | 通过-port参数指定端口号,也可以通 : |
Connected |
同时在服务器端会提示用户客户端已经连接成功。 |
SimpleSSLServer |
让我们先来看一下SimpleSS 对象;然后利用SSLServerSocke SimpleSSLServer对象。 | LServer。在main()方法中,程序 tFactory创建一个SimpleSSLServer | 获得了缺省的SSLServerSocketFactory 对象,最后调用start()方法启动 |
SSLServerSocketFactory ssf= |
(SSLServerSocketFactory)SSLServerSocketF | actory.getDefault(); |
SimpleSSLServer server=new SimpleSSLServ | er(ssf, port); |
server.start(); |
由于服务器是在一个单独的 启动了一个新的线程,该线程执 象,然后设定服务器需要进行客 | 线程中运行的,main()方法启动了 行run()方法中的代码。在run() 户端验证: | 服务器之后就退出了。start()方法 方法中创建了一个SSLServerSocket对 |
SSLServerSocket serverSo ServerSocket(port); | cket= (SSLServerSocket)serverSo | cketFactory.create |
serverSocket.setNeedClientAuth(true); |
调用run()方法后,程序 HandshakeCompletedListener对 的)。Socket的InputStream对 外一个线程中,用来将Socket接 体: | 进入了一个死循环,等待客户端的连 象(该对象是用来显示客户验证信息 象被包装在一个InputDisplayer对象 收到的数据发送到System.out。下面 | 接申请。循环中的每个Socket对应一个 中的标识名称[distinguished name] 中,这个InputDisplayer对象运行在另 的代码是SimpleSSLServer中的主循环 |
while (true) { |
String ident=String.valueOf(id++); |
//监听连接请求. |
SSLSocket socket= | (SSLSocket)serverSocket.accept() | ; |
//通过使用Handsha | keCompletedListener对象,程序进行 | 授权验证. |
HandshakeCompletedListener hcl=ne | w SimpleHandshakeListener(ident); |
socket.addHandsha | keCompletedListener(hcl); |
InputStream in=so | cket.getInputStream(); |
new InputDisplayer(ident, in); |
} |
程序中的SimpleHandshakeListener类实现了H SimpleHandshakeListener类中实现了handshakeCo 调用。它将显示出客户端的标识名称: | andshakeCompletedListerner接口。在 mpleted()方法,该方法在SSL握手阶段完成后将被JSSE |
class SimpleHandshakeLis | tener implements HandshakeComple | tedListener |
{ |
String ident; |
/** |
* 构造函数. |
*/ |
public SimpleHandshakeListener(Str | ing ident) |
{ |
this.ident=ident; |
} |
/**当SSL握手过程完成后该方法被激活. */ |
public void handsh | akeCompleted(HandshakeCompletedE | vent event) |
{ |
//显示授权信息. |
try { |
X509Certificate |
cert=(X509Ce | rtificate)event.getPeerCertifica | tes()[0]; |
String peer=cert.getSubjectDN( | ).getName(); |
System.out.pri | ntln(ident+": Request from "+pee | r); |
} |
catch (SSLPeerUnverifiedExceptio | n pue) { |
System.out.println(ident+": Pe | er unverified"); |
} |
} |
} |
用红色字体表示的两行代码 X509Certificated对象的数组。 素是客户端的验证信息,而最后 标识名称,并将它传送到System | 是这段代码的核心:getPeerCertifi 这些X509Certificated对象创建了客 一个通常是CA验证。当我们有了客户 .out。 | cates()方法返回一个 户端的身份标识。在数组中的第一个元 端的验证信息后。我们可以得到其中的 |
SimpleSSLClient |
SimpleSSLClient类比较简单,但是在后面的 getSLLSocketFactory()方法中,程序返回缺省 | 一些比较复杂的例子中的类会继承该类。在 的工厂类: |
protected SSLSocketFactory getSSLSocketF | actory() |
throws IOException, GeneralSecurityExc | eption |
{ |
return (SSLSocketFactory)SSLSocketFact | ory.getDefault(); |
} |
在runClient()方法中,程序处理了输入参数 接到服务器程序。在connect()方法中,程序首 startHandshang()方法启动和服务器端的握手过 HandshakeCompletedEvent事件。在服务器端的Han ,JSSE可以自动启动握手过程,但是必须是在第一 直到用户在键盘上输入信息后才会有数据通过Sock 们用startShake()方法来手工激活握手过程。 | 后,获得SSLSockFactory对象,调用connect()方法连 先创建一个SSLSocket对象,然后调用SSLSocket对象的 程。当握手过程完成后,会触发一个 dshakeCompletedListener对象会处理这个事件。事实上 次有数据通过Socket传输的情况下。由于在例子程序中, et传输,而我们希望服务器端及时报告连接情况,因此我 |
public void connect(SSLS | ocketFactory sf) throws IOExcept | ion |
{ |
socket=(SSLSocket)sf | .createSocket(host, port); |
try { |
socket.startHandshake(); |
} |
catch (IOException ioe) { |
// 握手失败.关闭连接. |
try { |
socket.close(); |
} |
catch (IOException ioe2) { |
// 忽略该错误. |
} |
socket=null; |
throw ioe; |
} |
} |
SimpleSSLClient类中的transmit()方法也 后将输出流包装到一个Writer对象中;最后将数据 | 很简单。首先程序将输入流包装到一个Reader对象中,然 流输出到Socket: |
boolean done=false; |
while (!done) { |
String line=reader.readLine(); |
if (line!=null) { |
writer.write(line); |
writer.write('/n'); |
writer.flush(); |
} |
else done=true; |
} |
定制KeyStore和TrustStore |
还记得我们是如何运行客户端的吗?我们需要 trustStore和trustStorePassword参数,以至于整 KeyStore和TrustStore,后面的例子中将告诉你如 SSLSocketFactory对象,其中每个SSLSocketFacto 这种技术,在同一个虚拟机上的所有安全连接都只 程序,这也许不会产生问题;但是对于那些比较大 | 在命令行中指定keyStore, keyStorePasword, 个命令显得过于冗长。事实上你可以在程序中指定 何实现这一点。同时在例子中还会演示如何配置多个 ry对象对应不同的KeyStore和TrustStore设置。如果没有 能使用同一个KeyStore和TrustStore。对于比较小的应用 的应用程序来说,这绝对是一个严重的缺陷。 |
在下面的例子中,我们将使用CustomTrustSto 先运行一下CustomTrustStoreClient: | reClient来动态定义KeyStore和TrustStore。首先让我们 |
java CustomTrustStoreClient |
为什么运行CustomTrustStoreClient时不需要 CustomTrustStoreClient的代码中指定了KeyStore 的密钥(password)。如果你想使用其他的KeySto -tspass参数来指定。下面让我们来看一下CustomT 法通过调用getTrustManager()方法获得一个Tur 获得一个KeyManager对象数组。然后利用得到的Tu 象,最后通过SSLContext对象的getSocketFactory 的init()方法时使用的参数。第一个参数是KeyM TrustManager数组。如果前两个参数被设定为null KeyStore来源于系统属性中的javax.net.ssl.keyS TrustStore来源于系统属性中的javax.net.ssl.tr 通过设定第三个参数可以指定JSSE中的随机数产生 数的产生是一个很敏感的问题,错误使用这个参数 。这样程序将使用缺省的并且是安全的SecureRand | 指定KeyStore和TrustStore参数呢?这是应为在 (ClientKeys)和TrustStore(ClientTruts)以及它们 re、 TrustStore或密钥,可以使用-ks、-kspass、-ts和 rustStoreClient的getSSLSocketFactory()方法。该方 stManager对象数组,通过调用getKeyManagers()方法 rstManager和KeyManager对象数组构造一个SSLContext对 ()方法来配置JSSE。需要注意的是在调用SSLContext类 anager对象数组。第二个参数和第一个参数类似,是 ,程序将使用缺省的KeyManager和TrustStore(缺省的 tore和javax.net.ssl.keyStorePassword属性;缺省的 ustStore和javax.net.ssl.trustStorePassword属性)。 器(Random Number Generate, RNG)。由于在SSL中随机 会导致安全连接变得不安全,因此我在例子中使用了null om对象。 |
protected SSLSocketFacto | ry getSSLSocketFactory() |
throws IOException, GeneralSecurityExc | eption |
{ |
// 调用getTrustManagers方法获得trust managers |
TrustManager[] tms=getTrustManagers(); |
// 调用getKeyManagers方法获得key manager |
KeyManager[] kms=getKeyManagers(); |
//利用KeyManagers创建一个SSLContext对 | 象.用获得的KeyStore和 |
// TrustStore初始化该S | SLContext对象.我们使用缺省的Secu | reRandom. |
SSLContext context=SSLContext.getInsta | nce("SSL"); |
context.init(kms, tms, null); |
//最后获得了SocketFactory对象. |
SSLSocketFactory ssf=c | ontext.getSocketFactory(); |
return ssf; |
} |
下面让我们看一看CustomKeyStoreClient类中 数组的: | 的getKeyMangers()方法是如何初始化KeyManagers对象 |
protected KeyManager[] getKeyManagers() |
throws IOException, GeneralSecurityExc | eption |
{ |
// 获得KeyManagerFactory对象. |
String alg=KeyManagerF | actory.getDefaultAlgorithm(); |
KeyManagerFactory kmFact=KeyManagerFac | tory.getInstance(alg); |
// 配置KeyManagerFactory对象使用的KeyS | toree.我们通过一个文件加载 |
// KeyStore. |
FileInputStream fis=ne | w FileInputStream(keyStore); |
KeyStore ks=KeyStore.getInstance("jks"); |
ks.load(fis, keyStorePassword.toCharAr | ray()); |
fis.close(); |
// 使用获得的KeyStore初始化KeyManagerFactory对象 |
kmFact.init(ks, keySto | rePassword.toCharArray()); |
// 获得KeyManagers对象 |
KeyManager[] kms=kmFact.getKeyManagers(); |
return kms; |
} |
首先的任务是获得一个KeyManagerFactory对 个缺省的KeyManagerFactory算法(程序员也可以 省算法)。获得KeyManagerFactory对象后就可以 将信息从文件送入KeyStore对象中。在这个过程之 的是jks)和密钥。当我们完成了KeyStore的加载 。通常在JSSE中,在KeyStore中的所有证书使用和 对象你可以突破这个限制。在初始化了KeyManager KeyManager对象数组。程序员通过使用和getKeyMa 这里我就不再重复了。 | 象,但是你必须知道应该使用哪种算法。JSSE中提供了一 通过指定ssl.KeyManagerFacotory.algorithm属性指定缺 加载KeyStore文件了,程序中通过一个InputStream对象 前,KeyStore对象需要知道输入流的格式(例子中我使用 后,我们就可以用它来初始化KeyManagerFactory对象了 KeyStore相同的密码,但是通过创建KeyManagerFactory Factory对象后,通常使用getKeyManager()方法来获得 ngers()方法类似的流程来初始化TrustManager数组, |
实现一个KeyManager类 |
到目前为止,我们已经知道如何在程序中动态 何实现一个KeyManager类。 | 生成KeyStore和TrustStore了。最后一个例子将告诉你如 |
当运行前几个例子的时候, 了两个人:Alice和Bob,在运行 你的计算机上情况会有所不同。 你能够在运行客户端时使用指定 需要在命令窗口中键入下面的命 | 不知道大家是否注意到服务器端显示 程序时JSSE会从中任选一个。在我的 下面让我们来看一看最后一个例子程 的授权。例如你需要指定使用Alice 令: | 的授权的标识名称。在前面我们授权给 计算机上JSSE选择的总是Bob,或许在 序:SelectAliasClient。这个例子使 的授权,由于Alice的别名是alice,你 |
java SelectAliasClient -alias alice |
当客户端和服务器端成功连接后,客户器端会出现下面的信息: |
1: New connection request |
1: Request from CN=Alice | , OU= Development, O=DCQ, L=Chon | gQing, |
ST=ChongQing, C=CH |
为了使程序使用指定的授权 KeyManager)。X509KeyManager 口获得授权的过程: | ,我们需要实现X509KeyManager接口 接口在SSL握手阶段使用了几个方法 | (X509KeyManager是JSSE中最常用的 来获得授权。下面是X509KeyManager接 |
1.JSSE调用chooseClientAlias()方法获得指定的授权。 |
2.chooseClientAlias()方法调用X509KeyMa 象使用的所有授权的别名,然后检查指定的授权别 | nager接口的getClientAlaises()方法获得SSLSocket对 名是否有效。 |
3.JSSE将别名作为参数调用X509KeyManager接 ,这样就获得了指定授权的相关信息。 | 口的getCertificateChain()和getPrivateKey()方法 |
在例子程序中,X509KeyMan 就是chooseClientAlias()方 | ager接口的实现类是AliasForcingKe 法。下面是该方法的源代码: | yManager。在该类中最重要的方法就是 |
public String chooseClie | ntAlias(String[] keyType, Princi | pal[] issuers, |
Socket socket) |
{ |
//对于每一种类型的授权,都需要调用一次getClientAliases()方法来验 |
// 证别名是否有效. |
boolean aliasFound=false; |
for (int i=0; i |
String[] validAl | iases=baseKM.getClientAliases(ke | yType[i], issuers); |
if (validAliases!=null) { |
for (int j=0; j |
if (validAliases | [j].equals(alias)) aliasFound=tr | ue; |
} |
} |
} |
if (aliasFound) return alias; |
else return null; |
} |
我们可以看到在程序中,chooserClientAlias 每次都针对不同的授权类型。AliasForingKeyMana 里就不再一一赘述了。 | ()方法实际上多次调用了getClientAliases()方法, ger还实现了X509KeyManager接口的其他五个方法,在这 |
然后我们就可以在程序中用 getSSLSocketFactory()方法 组,然后将其强制转化为AliasF 法的代码: | AliasForingKeyManager对象来替代K 中,我们只需要将通过调用getKeyMa orcingKeyManager对象就可以了。下 | eyManager对象了。在 nagers()方法获得KeyManager对象数 面是新的getSSLSocketFactory()方 |
protected SSLSocketFacto | ry getSSLSocketFactory() |
throws IOException, GeneralSecurityE | xception |
{ |
// 调用父类中的方法获得TrustManager和KeyManager |
KeyManager[] kms=getKeyManagers(); |
TrustManager[] tms=getTrustManagers(); |
// 如果指定了别名 | ,将KeyManagers包装在AliasForcin | gKeyManager对象中. |
if (alias!=null) { |
for (int i=0; i |
// 这里只处理了X509KeyManager接口 |
if (kms[i] ins | tanceof X509KeyManager) |
kms[i]=new A | liasForcingKeyManager((X509KeyMa | nager)kms[i], alias); |
} |
} |
// 利用TrustManagers和已经被包装的 | KeyManagers创建一个SSLContext对象. |
SSLContext context=SSLContext.getI | nstance("SSL"); |
context.init(kms, tms, null); |
// 获得SocketFactory对象. |
SSLSocketFactory ssf=context.getSo | cketFactory(); |
return ssf; |
} |
我们可以使用同样的方法来替换TrustManager 实现就留给读者朋友去解决了。 | 对象,这样我们就可以控制JSSE验证授权的机制。具体的 |
小结 |
在这篇文章中,我们讲述了使用JSSE的一些小 程实现下面的任务: | 技巧。读完这篇文章后,我相信大家因该知道如何通过编 |
· 使用HandshagCompletedListerner对象来获得关于连接的信息。 |
· 从SSLContext对象中获得一个SLLSocketFactory对象。 |
· 使用动态的TrustStroe或KeyStore。 |
· 突破在JSSE中KeySotre的密钥的每个授权的密钥必须相同的限制。 |
· 通过实现自己的KeyManager类来指定JSSE使用的授权。 |
如果大家有兴趣的话,还可以进一步将这些技 X509KeyManager接口,也可以在TrustStore和KeyS 己编写的TrustStore,KeyStore,TrustManager和 错误都可能导致SSL连接不再是安全的了。 | 术进行扩展。例如你可以在JSSE的其他类中使用 tore的实现类中从数据库中读取授权信息。但是在使用自 KeyManager的时候,需要非常小心,因为任何一个细微的 |