用JSSE定制SSL连接

JSSE(Java SecuritySocket Extension,Java安全套接字扩展)是Sun为了解决在Internet上的安全通讯而推出的解决方案。它实现了SSL和TSL(传输层安全)协议。在JSSE中包含了数据加密,服务器验证,消息完整性和客户端验证等技术。通过使用JSSE,开发人员可以在客户机和服务器之间通过TCP/IP协议安全地传输数据。这篇文章主要描述如何使用JSSE接口来控制SSL连接。

  首先我通过一个简单的客户机/服务器程序来介绍如何利用JSSE进行编程。当建立客户端时,我们需要配置KeyStore和 TrustStore文件,这样在程序中我们才可以从客户端的文件系统中加载它们。然后文章将讨论授权和身份验证方面的问题。通过从KeyStore中选择不同的授权,客户端程序可以连接到不同的服务器。

  在运行JSSE程序前,你需要正确安装JSSE。如果你安装了J2SE 1.4,JSSE已经被自动安装并配置好了,如果你使用的是其他版本的Java,你需要从官方站点上下载并安装JSSE,安装过程这里就不再赘述。由于 JSSE是在J2SE 1.4中才成为标准的,并且J2SE 1.4中的JSSE和以前的JSSE有一些细微的差别,而且文中的例子都是在J2SE 1.4下调试的,因此推荐你使用J2SE 1.4运行这些例子。

  在深入介绍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 clientKeys

  窗口中会出现下面的提示,根据提示输入相应的信息:

  输入keystore密码: password

  您的名字与姓氏是什么?

  [Unknown]: Alice

  您的组织单位名称是什么?

  [Unknown]: Development

  您的组织名称是什么?

  [Unknown]: DCQ

  您所在的城市或区域名称是什么?

  [Unknown]: ChongQing

  您所在的州或省份名称是什么?

  [Unknown]: ChongQing

  该单位的两字母国家代码是什么

  [Unknown]: CH

  CN=Alice, OU=Development, O=DCQ, L=ChongQing, ST=ChongQing, C=CH 正确吗?

  [否]: 是

  输入的主密码

  (如果和 keystore 密码相同,按回车):

  通过相同的方式可以建立对Bob的授权。

  keytool -genkey -alias bob -keystore clientKeys

  注意在名字与姓氏一栏中填写Bob。在完成后可以键入下面的命令来检测是否已经正确完成了授权。

  keytool -list -v -keystore clientKeys????

  · 一个服务器端的KeyStore文件,该文件中包含了对server的授权。

  在命令窗口中键入下面的命令:

  keytool -genkey -alias server -keystore serverKeys

  注意将密码设为password,名字与姓氏设定为Server。完成授权后同样可以通过上面提到的命令来检测。

  · 一个名为clientTrust的客户端TrustStore文件,该文件中包含了对server的授权。以及一个名为serverTrust的服务器端TrustStore文件,该文件中包含了对Alice和Bob的授权。

  keytool -export -alias server -keystore clientKeys -file server.cer

  输入keystore密码: password

  保存在文件中的认证

  keytool -export -alias alice -keystore clientKeys -file alice.cer

  输入keystore密码: password

  保存在文件中的认证

  keytool -export -alias bob -keystore clientKeys -file bob.cer

  输入keystore密码: password

  保存在文件中的认证

  这样keytool就在当前目录下创建了三个授权文件。然后我们将server.cer文件导入到clientTrust文件中;将alice.cer和bob.cer导入到serverTruest文件中:

  keytool -import -alias server -keystore clientTrust -file server.cer

  keytool -import -alias alice -keystore serverTrust -file alice.cer

  keytool -import -alias bob -keystore serverTrust-file bob.cer

  到目前为止,在当前目录下包含clientKeys,serverKeys,clientTrust,serverTrust四个文件。完成了KeyStore和TrustStore的设置后就可以运行例子程序了。首先需要运行服务器程序:

  java -Djavax.net.ssl.keyStore=serverKeys

  -Djavax.net.ssl.keyStorePassword=password

  -Djavax.net.ssl.trustStore#NAME?

  -Djavax.net.ssl.trustStorePassword=password SimpleSSLServer

  在命令行中我们指定了keyStore属性为serverKeys。由于服务器程序需要获得客户端的授权信息,我们指定trustStore为 serverTrust。这样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=password SimpleSSLClient

  客户端程序会试图向本机的49152端口建立SSL连接。同样你可以通过-port参数指定端口号,也可以通过-host参数指定主机名称。当连接成功后,会出现下面的提示信息:

  Connected

  同时在服务器端会提示用户客户端已经连接成功。

  SimpleSSLServer

  让我们先来看一下SimpleSSLServer。在main()方法中,程序获得了缺省的SSLServerSocketFactory对象;然后利用SSLServerSocketFactory创建一个SimpleSSLServer对象,最后调用start()方法启动 SimpleSSLServer对象。

  SSLServerSocketFactory ssf=

  (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();

  SimpleSSLServer server=new SimpleSSLServer(ssf,port);

  server.start();

  由于服务器是在一个单独的线程中运行的,main()方法启动了服务器之后就退出了。start()方法启动了一个新的线程,该线程执行run()方法中的代码。在run()方法中创建了一个SSLServerSocket对象,然后设定服务器需要进行客户端验证:

  SSLServerSocket serverSocket= (SSLServerSocket)serverSocketFactory.createServerSocket(port);

  serverSocket.setNeedClientAuth(true);

  调用run()方法后,程序进入了一个死循环,等待客户端的连接申请。循环中的每个Socket对应一个 HandshakeCompletedListener对象(该对象是用来显示客户验证信息中的标识名称[distinguished name]的)。Socket的InputStream对象被包装在一个InputDisplayer对象中,这个InputDisplayer对象运行在另外一个线程中,用来将Socket接收到的数据发送到System.out。下面的代码是SimpleSSLServer中的主循环体:

  while (true) {

  String ident=String.valueOf(id++);

  //监听连接请求.

  SSLSocket socket=(SSLSocket)serverSocket.accept();

  //通过使用HandshakeCompletedListener对象,程序进行授权验证.

  HandshakeCompletedListener hcl=new SimpleHandshakeListener(ident);

  socket.addHandshakeCompletedListener(hcl);

  InputStream in=socket.getInputStream();

  new InputDisplayer(ident, in);

  }

  程序中的SimpleHandshakeListener类实现了HandshakeCompletedListerner接口。在 SimpleHandshakeListener类中实现了handshakeCompleted()方法,该方法在SSL握手阶段完成后将被JSSE调用。它将显示出客户端的标识名称:

  class SimpleHandshakeListener implements HandshakeCompletedListener

  {

  String ident;

  /**

  * 构造函数.

  */

  public SimpleHandshakeListener(String ident)

  {

  this.ident=ident;

  }

  /**当SSL握手过程完成后该方法被激活. */

  public void handshakeCompleted(HandshakeCompletedEvent event)

  {

  //显示授权信息.

  try {

  X509Certificate

  cert=(X509Certificate)event.getPeerCertificates()[0];

  String peer=cert.getSubjectDN().getName();

  System.out.println(ident+": Request from "+peer);

  }

  catch (SSLPeerUnverifiedException pue) {

  System.out.println(ident+": Peer unverified");

  }

  }

  }

  用红色字体表示的两行代码是这段代码的核心:getPeerCertificates()方法返回一个X509Certificated对象的数组。这些X509Certificated对象创建了客户端的身份标识。在数组中的第一个元素是客户端的验证信息,而最后一个通常是CA验证。当我们有了客户端的验证信息后。我们可以得到其中的标识名称,并将它传送到System.out。

  SimpleSSLClient

  SimpleSSLClient类比较简单,但是在后面的一些比较复杂的例子中的类会继承该类。在getSLLSocketFactory()方法中,程序返回缺省的工厂类:

  protected SSLSocketFactory getSSLSocketFactory()

  throws IOException, GeneralSecurityException

  {

  return (SSLSocketFactory)SSLSocketFactory.getDefault();

  }

  在runClient()方法中,程序处理了输入参数后,获得SSLSockFactory对象,调用connect()方法连接到服务器程序。在connect()方法中,程序首先创建一个SSLSocket对象,然后调用SSLSocket对象的startHandshang()方法启动和服务器端的握手过程。当握手过程完成后,会触发一个HandshakeCompletedEvent事件。在服务器端的 HandshakeCompletedListener对象会处理这个事件。事实上,JSSE可以自动启动握手过程,但是必须是在第一次有数据通过 Socket传输的情况下。由于在例子程序中,直到用户在键盘上输入信息后才会有数据通过Socket传输,而我们希望服务器端及时报告连接情况,因此我们用startShake()方法来手工激活握手过程。

  public void connect(SSLSocketFactory sf) throws IOException

  {

  socket=(SSLSocket)sf.createSocket(host, port);

  try {

  socket.startHandshake();

  }

  catch (IOException ioe) {

  // 握手失败.关闭连接.

  try {

  socket.close();

  }

  catch (IOException ioe2) {

  // 忽略该错误.

  }

  socket=null;

  throw ioe;

  }

  }

  SimpleSSLClient类中的transmit()方法也很简单。首先程序将输入流包装到一个Reader对象中,然后将输出流包装到一个Writer对象中;最后将数据流输出到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?

  还记得我们是如何运行客户端的吗?我们需要在命令行中指定keyStore,keyStorePasword, trustStore和trustStorePassword参数,以至于整个命令显得过于冗长。事实上你可以在程序中指定KeyStore和 TrustStore,后面的例子中将告诉你如何实现这一点。同时在例子中还会演示如何配置多个SSLSocketFactory对象,其中每个 SSLSocketFactory对象对应不同的KeyStore和TrustStore设置。如果没有这种技术,在同一个虚拟机上的所有安全连接都只能使用同一个KeyStore和TrustStore。对于比较小的应用程序,这也许不会产生问题;但是对于那些比较大的应用程序来说,这绝对是一个严重的缺陷。

  在下面的例子中,我们将使用CustomTrustStoreClient来动态定义KeyStore和TrustStore。首先让我们先运行一下CustomTrustStoreClient:

  java CustomTrustStoreClient

  为什么运行CustomTrustStoreClient时不需要指定KeyStore和TrustStore参数呢?这是应为在 CustomTrustStoreClient的代码中指定了KeyStore(ClientKeys)和 TrustStore(ClientTruts)以及它们的密钥(password)。如果你想使用其他的KeyStore、 TrustStore或密钥,可以使用-ks、-kspass、-ts和-tspass参数来指定。下面让我们来看一下 CustomTrustStoreClient的getSSLSocketFactory()方法。该方法通过调用getTrustManager()方法获得一个TurstManager对象数组,通过调用getKeyManagers()方法获得一个KeyManager对象数组。然后利用得到的 TurstManager和KeyManager对象数组构造一个SSLContext对象,最后通过SSLContext对象的 getSocketFactory()方法来配置JSSE。需要注意的是在调用SSLContext类的init()方法时使用的参数。第一个参数是 KeyManager对象数组。第二个参数和第一个参数类似,是TrustManager数组。如果前两个参数被设定为null,程序将使用缺省的 KeyManager和TrustStore(缺省的KeyStore来源于系统属性中的javax.net.ssl.keyStore和 javax.net.ssl.keyStorePassword属性;缺省的TrustStore来源于系统属性中的 javax.net.ssl.trustStore和javax.net.ssl.trustStorePassword属性)。通过设定第三个参数可以指定JSSE中的随机数产生器(Random Number Generate, RNG)。由于在SSL中随机数的产生是一个很敏感的问题,错误使用这个参数会导致安全连接变得不安全,因此我在例子中使用了null。这样程序将使用缺省的并且是安全的SecureRandom对象。

  protected SSLSocketFactory getSSLSocketFactory()

  throws IOException, GeneralSecurityException

  {

  // 调用getTrustManagers方法获得trust managers

  TrustManager[] tms=getTrustManagers();

  // 调用getKeyManagers方法获得key manager

  KeyManager[] kms=getKeyManagers();

  //利用KeyManagers创建一个SSLContext对象.用获得的KeyStore和

  // TrustStore初始化该SSLContext对象.我们使用缺省的SecureRandom.

  SSLContext context=SSLContext.getInstance("SSL");

  context.init(kms, tms, null);

  //最后获得了SocketFactory对象.

  SSLSocketFactory ssf=context.getSocketFactory();

  return ssf;

  }

  下面让我们看一看CustomKeyStoreClient类中的getKeyMangers()方法是如何初始化KeyManagers对象数组的:

  protected KeyManager[] getKeyManagers()

  throws IOException, GeneralSecurityException

  {

  // 获得KeyManagerFactory对象.

  String alg=KeyManagerFactory.getDefaultAlgorithm();

  KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg);

  // 配置KeyManagerFactory对象使用的KeyStoree.我们通过一个文件加载

  // KeyStore.

  FileInputStream fis=new FileInputStream(keyStore);

  KeyStore ks=KeyStore.getInstance("jks");

  ks.load(fis, keyStorePassword.toCharArray());

  fis.close();

  // 使用获得的KeyStore初始化KeyManagerFactory对象

  kmFact.init(ks, keyStorePassword.toCharArray());

  // 获得KeyManagers对象

  KeyManager[] kms=kmFact.getKeyManagers();

  return kms;

  }

  首先的任务是获得一个KeyManagerFactory对象,但是你必须知道应该使用哪种算法。JSSE中提供了一个缺省的 KeyManagerFactory算法(程序员也可以通过指定ssl.KeyManagerFacotory.algorithm属性指定缺省算法)。获得KeyManagerFactory对象后就可以加载KeyStore文件了,程序中通过一个InputStream对象将信息从文件送入 KeyStore对象中。在这个过程之前,KeyStore对象需要知道输入流的格式(例子中我使用的是jks)和密钥。当我们完成了KeyStore的加载后,我们就可以用它来初始化KeyManagerFactory对象了。通常在JSSE中,在KeyStore中的所有证书使用和KeyStore相同的密码,但是通过创建KeyManagerFactory对象你可以突破这个限制。在初始化了KeyManagerFactory对象后,通常使用 getKeyManager()方法来获得KeyManager对象数组。程序员通过使用和getKeyMangers()方法类似的流程来初始化 TrustManager数组,这里我就不再重复了。

  实现一个KeyManager类

  到目前为止,我们已经知道如何在程序中动态生成KeyStore和TrustStore了。最后一个例子将告诉你如何实现一个KeyManager类。

  当运行前几个例子的时候,不知道大家是否注意到服务器端显示的授权的标识名称。在前面我们授权给了两个人:Alice和Bob,在运行程序时 JSSE会从中任选一个。在我的计算机上JSSE选择的总是Bob,或许在你的计算机上情况会有所不同。下面让我们来看一看最后一个例子程序:SelectAliasClient。这个例子使你能够在运行客户端时使用指定的授权。例如你需要指定使用Alice的授权,由于Alice的别名是 alice,你需要在命令窗口中键入下面的命令:

  java SelectAliasClient -alias alice

  当客户端和服务器端成功连接后,客户器端会出现下面的信息:

  1: New connection request

  1: Request from CN=Alice, OU= Development, O=DCQ, L=ChongQing,

  ST=ChongQing, C=CH

  为了使程序使用指定的授权,我们需要实现X509KeyManager接口(X509KeyManager是JSSE中最常用的 KeyManager)。X509KeyManager接口在SSL握手阶段使用了几个方法来获得授权。下面是X509KeyManager接口获得授权的过程:

  1.JSSE调用chooseClientAlias()方法获得指定的授权。

  2.chooseClientAlias()方法调用X509KeyManager接口的getClientAlaises()方法获得SSLSocket对象使用的所有授权的别名,然后检查指定的授权别名是否有效。

  3.JSSE将别名作为参数调用X509KeyManager接口的getCertificateChain()和getPrivateKey()方法,这样就获得了指定授权的相关信息。

  在例子程序中,X509KeyManager接口的实现类是AliasForcingKeyManager。在该类中最重要的方法就是就是chooseClientAlias()方法。下面是该方法的源代码:

  public String chooseClientAlias(String[] keyType, Principal[] issuers,

  Socket socket)

  {

  //对于每一种类型的授权,都需要调用一次getClientAliases()方法来验

  // 证别名是否有效.

  boolean aliasFound=false;

  for (int i=0; i< i++) !aliasFound; &&>

  String[] validAliases=baseKM.getClientAliases(keyType, issuers);

  if (validAliases!=null) {

  for (int j=0; j< !aliasFound; && j++)>

  if (validAliases[j].equals(alias)) aliasFound=true;

  }

  }

  }

  if (aliasFound) return alias;

  else return null;

  }

  我们可以看到在程序中,chooserClientAlias()方法实际上多次调用了getClientAliases()方法,每次都针对不同的授权类型。AliasForingKeyManager还实现了X509KeyManager接口的其他五个方法,在这里就不再一一赘述了。

  然后我们就可以在程序中用AliasForingKeyManager对象来替代KeyManager对象了。在 getSSLSocketFactory()方法中,我们只需要将通过调用getKeyManagers()方法获得KeyManager对象数组,然后将其强制转化为AliasForcingKeyManager对象就可以了。下面是新的getSSLSocketFactory()方法的代码:

  protected SSLSocketFactory getSSLSocketFactory()

  throws IOException, GeneralSecurityException

  {

  // 调用父类中的方法获得TrustManager和KeyManager

  KeyManager[] kms=getKeyManagers();

  TrustManager[]tms=getTrustManagers();

  // 如果指定了别名,将KeyManagers包装在AliasForcingKeyManager对象中.

  if (alias!=null) {

  for (int i=0; i< i++)>

  // 这里只处理了X509KeyManager接口

  if (kms instanceofX509KeyManager)

  kms=new AliasForcingKeyManager((X509KeyManager)kms,alias);

  }

  }

  // 利用TrustManagers和已经被包装的KeyManagers创建一个SSLContext对象.

  SSLContext context=SSLContext.getInstance("SSL");

  context.init(kms, tms, null);

  // 获得SocketFactory对象.

  SSLSocketFactory ssf=context.getSocketFactory();

  return ssf;

  }

  我们可以使用同样的方法来替换TrustManager对象,这样我们就可以控制JSSE验证授权的机制。具体的实现就留给读者朋友去解决了。

  小结

  在这篇文章中,我们讲述了使用JSSE的一些小技巧。读完这篇文章后,我相信大家因该知道如何通过编程实现下面的任务:

  · 使用HandshagCompletedListerner对象来获得关于连接的信息。

  · 从SSLContext对象中获得一个SLLSocketFactory对象。

  · 使用动态的TrustStroe或KeyStore。

  · 突破在JSSE中KeySotre的密钥的每个授权的密钥必须相同的限制。

  · 通过实现自己的KeyManager类来指定JSSE使用的授权。

  如果大家有兴趣的话,还可以进一步将这些技术进行扩展。例如你可以在JSSE的其他类中使用X509KeyManager接口,也可以在 TrustStore和KeyStore的实现类中从数据库中读取授权信息。但是在使用自己编写的 TrustStore,KeyStore,TrustManager和KeyManager的时候,需要非常小心,因为任何一个细微的错误都可能导致 SSL连接不再是安全的了。

  作者简介:冯睿毕业于美国北伊利诺大学计算机和电气工程系,获工程硕士学位。曾就职于NewMonics公司,进行Java虚拟机部分包的设计和开发和Java底层的性能优化工作。目前负责一些政府和企业级GIS系统的设计和实现。

 

 

JSSE(Java SecuritySocket Extension,Java安全套接字扩展)是Sun为了解决在Internet上的安全通讯而推出的解决方案。它实现了SSL和TSL(传输层安全)协议。在JSSE中包含了数据加密,服务器验证,消息完整性和客户端验证等技术。通过使用JSSE,开发人员可以在客户机和服务器之间通过TCP/IP协议安全地传输数据。这篇文章主要描述如何使用JSSE接口来控制SSL连接。

  首先我通过一个简单的客户机/服务器程序来介绍如何利用JSSE进行编程。当建立客户端时,我们需要配置KeyStore和 TrustStore文件,这样在程序中我们才可以从客户端的文件系统中加载它们。然后文章将讨论授权和身份验证方面的问题。通过从KeyStore中选择不同的授权,客户端程序可以连接到不同的服务器。

  在运行JSSE程序前,你需要正确安装JSSE。如果你安装了J2SE 1.4,JSSE已经被自动安装并配置好了,如果你使用的是其他版本的Java,你需要从官方站点上下载并安装JSSE,安装过程这里就不再赘述。由于 JSSE是在J2SE 1.4中才成为标准的,并且J2SE 1.4中的JSSE和以前的JSSE有一些细微的差别,而且文中的例子都是在J2SE 1.4下调试的,因此推荐你使用J2SE 1.4运行这些例子。

  在深入介绍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 clientKeys

  窗口中会出现下面的提示,根据提示输入相应的信息:

  输入keystore密码: password

  您的名字与姓氏是什么?

  [Unknown]: Alice

  您的组织单位名称是什么?

  [Unknown]: Development

  您的组织名称是什么?

  [Unknown]: DCQ

  您所在的城市或区域名称是什么?

  [Unknown]: ChongQing

  您所在的州或省份名称是什么?

  [Unknown]: ChongQing

  该单位的两字母国家代码是什么

  [Unknown]: CH

  CN=Alice, OU=Development, O=DCQ, L=ChongQing, ST=ChongQing, C=CH 正确吗?

  [否]: 是

  输入的主密码

  (如果和 keystore 密码相同,按回车):

  通过相同的方式可以建立对Bob的授权。

  keytool -genkey -alias bob -keystore clientKeys

  注意在名字与姓氏一栏中填写Bob。在完成后可以键入下面的命令来检测是否已经正确完成了授权。

  keytool -list -v -keystore clientKeys????

  · 一个服务器端的KeyStore文件,该文件中包含了对server的授权。

  在命令窗口中键入下面的命令:

  keytool -genkey -alias server -keystore serverKeys

  注意将密码设为password,名字与姓氏设定为Server。完成授权后同样可以通过上面提到的命令来检测。

  · 一个名为clientTrust的客户端TrustStore文件,该文件中包含了对server的授权。以及一个名为serverTrust的服务器端TrustStore文件,该文件中包含了对Alice和Bob的授权。

  keytool -export -alias server -keystore clientKeys -file server.cer

  输入keystore密码: password

  保存在文件中的认证

  keytool -export -alias alice -keystore clientKeys -file alice.cer

  输入keystore密码: password

  保存在文件中的认证

  keytool -export -alias bob -keystore clientKeys -file bob.cer

  输入keystore密码: password

  保存在文件中的认证

  这样keytool就在当前目录下创建了三个授权文件。然后我们将server.cer文件导入到clientTrust文件中;将alice.cer和bob.cer导入到serverTruest文件中:

  keytool -import -alias server -keystore clientTrust -file server.cer

  keytool -import -alias alice -keystore serverTrust -file alice.cer

  keytool -import -alias bob -keystore serverTrust-file bob.cer

  到目前为止,在当前目录下包含clientKeys,serverKeys,clientTrust,serverTrust四个文件。完成了KeyStore和TrustStore的设置后就可以运行例子程序了。首先需要运行服务器程序:

  java -Djavax.net.ssl.keyStore=serverKeys

  -Djavax.net.ssl.keyStorePassword=password

  -Djavax.net.ssl.trustStore#NAME?

  -Djavax.net.ssl.trustStorePassword=password SimpleSSLServer

  在命令行中我们指定了keyStore属性为serverKeys。由于服务器程序需要获得客户端的授权信息,我们指定trustStore为 serverTrust。这样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=password SimpleSSLClient

  客户端程序会试图向本机的49152端口建立SSL连接。同样你可以通过-port参数指定端口号,也可以通过-host参数指定主机名称。当连接成功后,会出现下面的提示信息:

  Connected

  同时在服务器端会提示用户客户端已经连接成功。

  SimpleSSLServer

  让我们先来看一下SimpleSSLServer。在main()方法中,程序获得了缺省的SSLServerSocketFactory对象;然后利用SSLServerSocketFactory创建一个SimpleSSLServer对象,最后调用start()方法启动 SimpleSSLServer对象。

  SSLServerSocketFactory ssf=

  (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();

  SimpleSSLServer server=new SimpleSSLServer(ssf,port);

  server.start();

  由于服务器是在一个单独的线程中运行的,main()方法启动了服务器之后就退出了。start()方法启动了一个新的线程,该线程执行run()方法中的代码。在run()方法中创建了一个SSLServerSocket对象,然后设定服务器需要进行客户端验证:

  SSLServerSocket serverSocket= (SSLServerSocket)serverSocketFactory.createServerSocket(port);

  serverSocket.setNeedClientAuth(true);

  调用run()方法后,程序进入了一个死循环,等待客户端的连接申请。循环中的每个Socket对应一个 HandshakeCompletedListener对象(该对象是用来显示客户验证信息中的标识名称[distinguished name]的)。Socket的InputStream对象被包装在一个InputDisplayer对象中,这个InputDisplayer对象运行在另外一个线程中,用来将Socket接收到的数据发送到System.out。下面的代码是SimpleSSLServer中的主循环体:

  while (true) {

  String ident=String.valueOf(id++);

  //监听连接请求.

  SSLSocket socket=(SSLSocket)serverSocket.accept();

  //通过使用HandshakeCompletedListener对象,程序进行授权验证.

  HandshakeCompletedListener hcl=new SimpleHandshakeListener(ident);

  socket.addHandshakeCompletedListener(hcl);

  InputStream in=socket.getInputStream();

  new InputDisplayer(ident, in);

  }

  程序中的SimpleHandshakeListener类实现了HandshakeCompletedListerner接口。在 SimpleHandshakeListener类中实现了handshakeCompleted()方法,该方法在SSL握手阶段完成后将被JSSE调用。它将显示出客户端的标识名称:

  class SimpleHandshakeListener implements HandshakeCompletedListener

  {

  String ident;

  /**

  * 构造函数.

  */

  public SimpleHandshakeListener(String ident)

  {

  this.ident=ident;

  }

  /**当SSL握手过程完成后该方法被激活. */

  public void handshakeCompleted(HandshakeCompletedEvent event)

  {

  //显示授权信息.

  try {

  X509Certificate

  cert=(X509Certificate)event.getPeerCertificates()[0];

  String peer=cert.getSubjectDN().getName();

  System.out.println(ident+": Request from "+peer);

  }

  catch (SSLPeerUnverifiedException pue) {

  System.out.println(ident+": Peer unverified");

  }

  }

  }

  用红色字体表示的两行代码是这段代码的核心:getPeerCertificates()方法返回一个X509Certificated对象的数组。这些X509Certificated对象创建了客户端的身份标识。在数组中的第一个元素是客户端的验证信息,而最后一个通常是CA验证。当我们有了客户端的验证信息后。我们可以得到其中的标识名称,并将它传送到System.out。

  SimpleSSLClient

  SimpleSSLClient类比较简单,但是在后面的一些比较复杂的例子中的类会继承该类。在getSLLSocketFactory()方法中,程序返回缺省的工厂类:

  protected SSLSocketFactory getSSLSocketFactory()

  throws IOException, GeneralSecurityException

  {

  return (SSLSocketFactory)SSLSocketFactory.getDefault();

  }

  在runClient()方法中,程序处理了输入参数后,获得SSLSockFactory对象,调用connect()方法连接到服务器程序。在connect()方法中,程序首先创建一个SSLSocket对象,然后调用SSLSocket对象的startHandshang()方法启动和服务器端的握手过程。当握手过程完成后,会触发一个HandshakeCompletedEvent事件。在服务器端的 HandshakeCompletedListener对象会处理这个事件。事实上,JSSE可以自动启动握手过程,但是必须是在第一次有数据通过 Socket传输的情况下。由于在例子程序中,直到用户在键盘上输入信息后才会有数据通过Socket传输,而我们希望服务器端及时报告连接情况,因此我们用startShake()方法来手工激活握手过程。

  public void connect(SSLSocketFactory sf) throws IOException

  {

  socket=(SSLSocket)sf.createSocket(host, port);

  try {

  socket.startHandshake();

  }

  catch (IOException ioe) {

  // 握手失败.关闭连接.

  try {

  socket.close();

  }

  catch (IOException ioe2) {

  // 忽略该错误.

  }

  socket=null;

  throw ioe;

  }

  }

  SimpleSSLClient类中的transmit()方法也很简单。首先程序将输入流包装到一个Reader对象中,然后将输出流包装到一个Writer对象中;最后将数据流输出到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?

  还记得我们是如何运行客户端的吗?我们需要在命令行中指定keyStore,keyStorePasword, trustStore和trustStorePassword参数,以至于整个命令显得过于冗长。事实上你可以在程序中指定KeyStore和 TrustStore,后面的例子中将告诉你如何实现这一点。同时在例子中还会演示如何配置多个SSLSocketFactory对象,其中每个 SSLSocketFactory对象对应不同的KeyStore和TrustStore设置。如果没有这种技术,在同一个虚拟机上的所有安全连接都只能使用同一个KeyStore和TrustStore。对于比较小的应用程序,这也许不会产生问题;但是对于那些比较大的应用程序来说,这绝对是一个严重的缺陷。

  在下面的例子中,我们将使用CustomTrustStoreClient来动态定义KeyStore和TrustStore。首先让我们先运行一下CustomTrustStoreClient:

  java CustomTrustStoreClient

  为什么运行CustomTrustStoreClient时不需要指定KeyStore和TrustStore参数呢?这是应为在 CustomTrustStoreClient的代码中指定了KeyStore(ClientKeys)和 TrustStore(ClientTruts)以及它们的密钥(password)。如果你想使用其他的KeyStore、 TrustStore或密钥,可以使用-ks、-kspass、-ts和-tspass参数来指定。下面让我们来看一下 CustomTrustStoreClient的getSSLSocketFactory()方法。该方法通过调用getTrustManager()方法获得一个TurstManager对象数组,通过调用getKeyManagers()方法获得一个KeyManager对象数组。然后利用得到的 TurstManager和KeyManager对象数组构造一个SSLContext对象,最后通过SSLContext对象的 getSocketFactory()方法来配置JSSE。需要注意的是在调用SSLContext类的init()方法时使用的参数。第一个参数是 KeyManager对象数组。第二个参数和第一个参数类似,是TrustManager数组。如果前两个参数被设定为null,程序将使用缺省的 KeyManager和TrustStore(缺省的KeyStore来源于系统属性中的javax.net.ssl.keyStore和 javax.net.ssl.keyStorePassword属性;缺省的TrustStore来源于系统属性中的 javax.net.ssl.trustStore和javax.net.ssl.trustStorePassword属性)。通过设定第三个参数可以指定JSSE中的随机数产生器(Random Number Generate, RNG)。由于在SSL中随机数的产生是一个很敏感的问题,错误使用这个参数会导致安全连接变得不安全,因此我在例子中使用了null。这样程序将使用缺省的并且是安全的SecureRandom对象。

  protected SSLSocketFactory getSSLSocketFactory()

  throws IOException, GeneralSecurityException

  {

  // 调用getTrustManagers方法获得trust managers

  TrustManager[] tms=getTrustManagers();

  // 调用getKeyManagers方法获得key manager

  KeyManager[] kms=getKeyManagers();

  //利用KeyManagers创建一个SSLContext对象.用获得的KeyStore和

  // TrustStore初始化该SSLContext对象.我们使用缺省的SecureRandom.

  SSLContext context=SSLContext.getInstance("SSL");

  context.init(kms, tms, null);

  //最后获得了SocketFactory对象.

  SSLSocketFactory ssf=context.getSocketFactory();

  return ssf;

  }

  下面让我们看一看CustomKeyStoreClient类中的getKeyMangers()方法是如何初始化KeyManagers对象数组的:

  protected KeyManager[] getKeyManagers()

  throws IOException, GeneralSecurityException

  {

  // 获得KeyManagerFactory对象.

  String alg=KeyManagerFactory.getDefaultAlgorithm();

  KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg);

  // 配置KeyManagerFactory对象使用的KeyStoree.我们通过一个文件加载

  // KeyStore.

  FileInputStream fis=new FileInputStream(keyStore);

  KeyStore ks=KeyStore.getInstance("jks");

  ks.load(fis, keyStorePassword.toCharArray());

  fis.close();

  // 使用获得的KeyStore初始化KeyManagerFactory对象

  kmFact.init(ks, keyStorePassword.toCharArray());

  // 获得KeyManagers对象

  KeyManager[] kms=kmFact.getKeyManagers();

  return kms;

  }

  首先的任务是获得一个KeyManagerFactory对象,但是你必须知道应该使用哪种算法。JSSE中提供了一个缺省的 KeyManagerFactory算法(程序员也可以通过指定ssl.KeyManagerFacotory.algorithm属性指定缺省算法)。获得KeyManagerFactory对象后就可以加载KeyStore文件了,程序中通过一个InputStream对象将信息从文件送入 KeyStore对象中。在这个过程之前,KeyStore对象需要知道输入流的格式(例子中我使用的是jks)和密钥。当我们完成了KeyStore的加载后,我们就可以用它来初始化KeyManagerFactory对象了。通常在JSSE中,在KeyStore中的所有证书使用和KeyStore相同的密码,但是通过创建KeyManagerFactory对象你可以突破这个限制。在初始化了KeyManagerFactory对象后,通常使用 getKeyManager()方法来获得KeyManager对象数组。程序员通过使用和getKeyMangers()方法类似的流程来初始化 TrustManager数组,这里我就不再重复了。

  实现一个KeyManager类

  到目前为止,我们已经知道如何在程序中动态生成KeyStore和TrustStore了。最后一个例子将告诉你如何实现一个KeyManager类。

  当运行前几个例子的时候,不知道大家是否注意到服务器端显示的授权的标识名称。在前面我们授权给了两个人:Alice和Bob,在运行程序时 JSSE会从中任选一个。在我的计算机上JSSE选择的总是Bob,或许在你的计算机上情况会有所不同。下面让我们来看一看最后一个例子程序:SelectAliasClient。这个例子使你能够在运行客户端时使用指定的授权。例如你需要指定使用Alice的授权,由于Alice的别名是 alice,你需要在命令窗口中键入下面的命令:

  java SelectAliasClient -alias alice

  当客户端和服务器端成功连接后,客户器端会出现下面的信息:

  1: New connection request

  1: Request from CN=Alice, OU= Development, O=DCQ, L=ChongQing,

  ST=ChongQing, C=CH

  为了使程序使用指定的授权,我们需要实现X509KeyManager接口(X509KeyManager是JSSE中最常用的 KeyManager)。X509KeyManager接口在SSL握手阶段使用了几个方法来获得授权。下面是X509KeyManager接口获得授权的过程:

  1.JSSE调用chooseClientAlias()方法获得指定的授权。

  2.chooseClientAlias()方法调用X509KeyManager接口的getClientAlaises()方法获得SSLSocket对象使用的所有授权的别名,然后检查指定的授权别名是否有效。

  3.JSSE将别名作为参数调用X509KeyManager接口的getCertificateChain()和getPrivateKey()方法,这样就获得了指定授权的相关信息。

  在例子程序中,X509KeyManager接口的实现类是AliasForcingKeyManager。在该类中最重要的方法就是就是chooseClientAlias()方法。下面是该方法的源代码:

  public String chooseClientAlias(String[] keyType, Principal[] issuers,

  Socket socket)

  {

  //对于每一种类型的授权,都需要调用一次getClientAliases()方法来验

  // 证别名是否有效.

  boolean aliasFound=false;

  for (int i=0; i< i++) !aliasFound; &&>

  String[] validAliases=baseKM.getClientAliases(keyType, issuers);

  if (validAliases!=null) {

  for (int j=0; j< !aliasFound; && j++)>

  if (validAliases[j].equals(alias)) aliasFound=true;

  }

  }

  }

  if (aliasFound) return alias;

  else return null;

  }

  我们可以看到在程序中,chooserClientAlias()方法实际上多次调用了getClientAliases()方法,每次都针对不同的授权类型。AliasForingKeyManager还实现了X509KeyManager接口的其他五个方法,在这里就不再一一赘述了。

  然后我们就可以在程序中用AliasForingKeyManager对象来替代KeyManager对象了。在 getSSLSocketFactory()方法中,我们只需要将通过调用getKeyManagers()方法获得KeyManager对象数组,然后将其强制转化为AliasForcingKeyManager对象就可以了。下面是新的getSSLSocketFactory()方法的代码:

  protected SSLSocketFactory getSSLSocketFactory()

  throws IOException, GeneralSecurityException

  {

  // 调用父类中的方法获得TrustManager和KeyManager

  KeyManager[] kms=getKeyManagers();

  TrustManager[]tms=getTrustManagers();

  // 如果指定了别名,将KeyManagers包装在AliasForcingKeyManager对象中.

  if (alias!=null) {

  for (int i=0; i< i++)>

  // 这里只处理了X509KeyManager接口

  if (kms instanceofX509KeyManager)

  kms=new AliasForcingKeyManager((X509KeyManager)kms,alias);

  }

  }

  // 利用TrustManagers和已经被包装的KeyManagers创建一个SSLContext对象.

  SSLContext context=SSLContext.getInstance("SSL");

  context.init(kms, tms, null);

  // 获得SocketFactory对象.

  SSLSocketFactory ssf=context.getSocketFactory();

  return ssf;

  }

  我们可以使用同样的方法来替换TrustManager对象,这样我们就可以控制JSSE验证授权的机制。具体的实现就留给读者朋友去解决了。

  小结

  在这篇文章中,我们讲述了使用JSSE的一些小技巧。读完这篇文章后,我相信大家因该知道如何通过编程实现下面的任务:

  · 使用HandshagCompletedListerner对象来获得关于连接的信息。

  · 从SSLContext对象中获得一个SLLSocketFactory对象。

  · 使用动态的TrustStroe或KeyStore。

  · 突破在JSSE中KeySotre的密钥的每个授权的密钥必须相同的限制。

  · 通过实现自己的KeyManager类来指定JSSE使用的授权。

  如果大家有兴趣的话,还可以进一步将这些技术进行扩展。例如你可以在JSSE的其他类中使用X509KeyManager接口,也可以在 TrustStore和KeyStore的实现类中从数据库中读取授权信息。但是在使用自己编写的 TrustStore,KeyStore,TrustManager和KeyManager的时候,需要非常小心,因为任何一个细微的错误都可能导致 SSL连接不再是安全的了。

  作者简介:冯睿毕业于美国北伊利诺大学计算机和电气工程系,获工程硕士学位。曾就职于NewMonics公司,进行Java虚拟机部分包的设计和开发和Java底层的性能优化工作。目前负责一些政府和企业级GIS系统的设计和实现。

 

转自 http://www.router.net.cn/Article/18290.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值