转载地址:http://blog.csdn.net/yangdelong/article/details/4575983
SSL(安全套接层)是 Netscape公司在1994年开发的,最初用于WEB浏览器,为浏览器与服务器间的数据传递提供安全保障,提供了加密、来源认证和数据完整性的功能。现在SSL3.0得到了普遍的使用,它的改进版TLS(传输层安全)已经成为互联网标准。SSL本身和TCP套接字连接是很相似的,在协议栈中,SSL可以被简单的看作是安全的TCP连接,但是某些TCP连接的特性它是不支持的,比如带外数据(out-of-bound)。
在构建基于 Socket的C/S程序时,通过添加对SSL的支持来保障数据安全和完整是不错的方法。完善的Java为我们提供了简单的实现方法:JSSE(Java 安全套接字扩展)。JSSE是一个纯Java实现的SSL和TLS协议框架,抽象了SSL和TLS复杂的算法,使安全问题变得简单。JSSE已经成为 J2SE1.4版本中的标准组件,支持SSL 3.0和TLS 1.0。我们将通过一个具体的例子演示JSSE的一些基本应用。例子中的服务器端将打开一个SSL Socket,只有持有指定证书的客户端可以与它连接,所有的数据传递都是加密的。
构造一个SSLSocket是非常简单的:
SSLServerSocketFactory factory=(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SSLServerSocket server = (SSLServerSocket) factory.createServerSocket(portNumber);
SSLSocket socket = (SSLSocket);
但是执行这样的程序会产生一个异常,报告找不到可信任的证书。SSLSocket和普通的Socket是不一样的,它需要一个证书来进行安全认证。
一、 证书
生成一个CA证书,在命令行下执行:
keytool –genkey –keystore SSLKey –keyalg rsa –alias SSL
黑体部分是用户可以自己指定的参数,第一个参数是要生成的证书的名字,第二个参数是证书的别名。rsa指明了我们使用的加密方法。
系统会要求输入证书发放者的信息,逐项输入即可
系统生成的文件命将会和证书名相同。证书可以提交给权威CA认证组织审核,如果通过审核,组织会提供信任担保,向客户担保你的连接是安全的。当然这不是必须的。在我们的例子中会把证书直接打包到客户端程序中,保证客户端是授权用户,避免伪造客户,所以不需要提交审核。
二、 服务器端
现在可以编写服务器端的代码,与普通的Socket代码不同,我们需要在程序中导入证书,并使用该证书构造SSLSocket。需要的说明的是:
●KeyStore ks=KeyStore.getInstance("JKS");
访问Java密钥库,JKS是keytool创建的Java密钥库,保存密钥。
● KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509");
创建用于管理JKS密钥库的X.509密钥管理器。
● SSLContext sslContext=SSLContext.getInstance("SSLv3");
构造SSL环境,指定SSL版本为3.0,也可以使用TLSv1,但是SSLv3更加常用。
●sslContext.init(kmf.getKeyManagers(),null,null);
初始化SSL环境。第二个参数是告诉JSSE使用的可信任证书的来源,设置为null是从javax.net.ssl.trustStore中获得证书。第三个参数是JSSE生成的随机数,这个参数将影响系统的安全性,设置为null是个好选择,可以保证JSSE的安全性。
完整代码如下:
/* *SSL Socket的服务器端 *@Author Bromon */ package org.ec107.ssl; import java.net.*; import javax.net.ssl.*; import java.io.*; import java.security.*; public class SSLServer { static int port=8266; //系统将要监听的端口号,82.6.6是偶以前女朋友的生日^_^ static SSLServerSocket server; /* *构造函数 */ public SSLServer() { } /* *@param port 监听的端口号 *@return 返回一个SSLServerSocket对象 */ private static SSLServerSocket getServerSocket(int thePort) { SSLServerSocket s=null; try { String key="SSLKey"; //要使用的证书名 char keyStorePass[]="12345678".toCharArray(); //证书密码 char keyPassword[]="12345678".toCharArray(); //证书别称所使用的主要密码 KeyStore ks=KeyStore.getInstance("JKS"); //创建JKS密钥库 ks.load(new FileInputStream(key),keyStorePass); //创建管理JKS密钥库的X.509密钥管理器 KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509"); kmf.init(ks,keyPassword); SSLContext sslContext=SSLContext.getInstance("SSLv3"); sslContext.init(kmf.getKeyManagers(),null,null); //根据上面配置的SSL上下文来产生SSLServerSocketFactory,与通常的产生方法不同 SSLServerSocketFactory factory=sslContext.getServerSocketFactory(); s=(SSLServerSocket)factory.createServerSocket(thePort); }catch(Exception e) { System.out.println(e); } return(s); } public static void main(String args[]) { try { server=getServerSocket(port); System.out.println("在”+port+”端口等待连接..."); while(true) { SSLSocket socket=(SSLSocket)server.accept(); //将得到的socket交给CreateThread对象处理,主线程继续监听 new CreateThread(socket); } }catch(Exception e) { System.out.println("main方法错误80:"+e); } } } /* *内部类,获得主线程的socket连接,生成子线程来处理 */ class CreateThread extends Thread { static BufferedReader in; static PrintWriter out; static Socket s; /* *构造函数,获得socket连接,初始化in和out对象 */ public CreateThread(Socket socket) { try { s=socket; in=new BufferedReader(new InputStreamReader(s.getInputStream(),"gb2312")); out=new PrintWriter(s.getOutputStream(),true); start(); //开新线程执行run方法 }catch(Exception e) { System.out.println(e); } } /* *线程方法,处理socket传递过来的数据 */ public void run() { try { String msg=in.readLine(); System.out.println(msg); s.close(); }catch(Exception e) { System.out.println(e); } } } |
/* *SSL Socket 的客户端 *@Author Bromon */ package org.ec107.ssl; import java.net.*; import javax.net.ssl.*; import javax.net.*; import java.io.*; public class SSLClient { static int port=8266; public static void main(String args[]) { try { SSLSocketFactory factory=(SSLSocketFactory)SSLSocketFactory.getDefault(); Socket s=factory.createSocket("localhost",port); PrintWriter out=new PrintWriter(s.getOutputStream(),true); out.println("安全的说你好"); out.close(); s.close(); }catch(Exception e) { System.out.println(e); } } } |
public class GetJavaHome { public static void main(String args[]) { System.out.println(System.getProperty(“java.home”)); } } |
所谓认证,是要对某台(当然可以是集群)服务器身份做认证.
认证方式有两种:
自签名认证:服务端生成key,然后根据key导出证书.公布于站点.
通过第三方认证机构认证:有服务端生成key,然后导出认证信息,交由天威诚信等第三方认证机构认证,最后生成证书,公布于站点.
客户端,将证书下载,确认为可信任公司认证信息,并且导入到受信任区(trustscore),建立连接与服务端进行正常交互.至此,就完成了对服务端的认证.
所以客户端必须将证书导入信任区.
java中,可以查看证书:
写个简单的方法:
FileInputStream fis = new FileInputStream("cert.cer");
CertificateFactory cf=CertificateFactory.getInstance("X509");
X509Certificate c=(X509Certificate) cf.generateCertificate(fis);
System.out.println(c.getSubjectDN());
//可以查看下X509Certificate的一些get方法.
其实keytool仅仅是jdk中的工具.
openssl是更常用的一个工具
SSL双向认证java实现
http://www.blogjava.net/stone2083/archive/2008/07/10/169015.html
本文通过模拟场景,介绍SSL双向认证的java实现
默认的情况下,我认为读者已经对SSL原理有一定的了解,所以文章中对SSL的原理,不做详细的介绍。
如果有这个需要,那么通过GOOGLE,可以搜索到很多这样的文章。
模拟场景:
Server端和Client端通信,需要进行授权和身份的验证,即Client只能接受Server的消息,Server只能接受Client的消息。
实现技术:
JSSE(Java Security Socket Extension)
是Sun为了解决在Internet上的安全通讯而推出的解决方案。它实现了SSL和TSL(传输层安全)协议。在JSSE中包含了数据加密,服务器验证,消息完整性和客户端验证等技术。通过使用JSSE,开发人员可以在客户机和服务器之间通过TCP/IP协议安全地传输数据
为了实现消息认证。
Server需要:
1)KeyStore: 其中保存服务端的私钥
2)Trust KeyStore:其中保存客户端的授权证书
同样,Client需要:
1)KeyStore:其中保存客户端的私钥
2)Trust KeyStore:其中保存服务端的授权证书
我们可以使用Java自带的keytool命令,去生成这样信息文件
1)生成服务端私钥,并且导入到服务端KeyStore文件中
keytool -genkey -alias serverkey -keystore kserver.keystore
过程中,分别需要填写,根据需求自己设置就行
keystore密码:123456
名字和姓氏:stone
组织单位名称:eulic
组织名称:eulic
城市或区域名称:HZ
州或省份名称:ZJ
国家代码:CN
serverkey私钥的密码,不填写和keystore的密码一致:123456
就可以生成kserver.keystore文件
server.keystore是给服务端用的,其中保存着自己的私钥
2)根据私钥,导出服务端证书
keytool -export -alias serverkey -keystore kserver.keystore -file server.crt
server.crt就是服务端的证书
3)将服务端证书,导入到客户端的Trust KeyStore中
keytool -import -alias serverkey -file server.crt -keystore tclient.keystore
tclient.keystore是给客户端用的,其中保存着受信任的证书
采用同样的方法,生成客户端的私钥,客户端的证书,并且导入到服务端的Trust KeyStore中
1)keytool -genkey -alias clientkey -keystore kclient.keystore
2)keytool -export -alias clientkey -keystore kclient.keystore -file client.crt
3)keytool -import -alias clientkey -file client.crt -keystore tserver.keystore
如此一来,生成的文件分成两组
服务端保存:kserver.keystore tserver.keystore
客户端保存:kclient.keystore tclient.kyestore
接下来,就采用JSSE,分别生成SSLServerSocket,SSLSocket
服务端,生成SSLServerSocket代码
SSLContext ctx = SSLContext.getInstance("SSL");
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
KeyStore ks = KeyStore.getInstance("JKS");
KeyStore tks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("data/kserver.keystore"), SERVER_KEY_STORE_PASSWORD.toCharArray());
tks.load(new FileInputStream("data/tserver.keystore"), SERVER_TRUST_KEY_STORE_PASSWORD.toCharArray());
kmf.init(ks, SERVER_KEY_STORE_PASSWORD.toCharArray());
tmf.init(tks);
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
SSLServerSocket serverSocket = (SSLServerSocket) ctx.getServerSocketFactory().createServerSocket(DEFAULT_PORT);
serverSocket.setNeedClientAuth(true); //表明需要验证客户端的身份。
return serverSocket;
客户端,生成SSLSocket的代码,大同小异
SSLContext ctx = SSLContext.getInstance("SSL");
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
KeyStore ks = KeyStore.getInstance("JKS");
KeyStore tks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("data/kclient.keystore"), CLIENT_KEY_STORE_PASSWORD.toCharArray());
tks.load(new FileInputStream("data/tclient.keystore"), CLIENT_TRUST_KEY_STORE_PASSWORD.toCharArray());
kmf.init(ks, CLIENT_KEY_STORE_PASSWORD.toCharArray());
tmf.init(tks);
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return (SSLSocket) ctx.getSocketFactory().createSocket(DEFAULT_HOST, DEFAULT_PORT);
如此,就完成了服务端和客户端之间的基于身份认证的交互。
client采用kclient.keystore中的clientkey私钥进行数据加密,发送给server
server采用tserver.keystore中的client.crt证书(包含了clientkey的公钥)对数据解密,如果解密成功,证明消息来自client,进行逻辑处理
server采用kserver.keystore中的serverkey私钥进行数据叫米,发送给client
client采用tclient.keystore中的server.crt证书(包含了serverkey的公钥)对数据解密,如果解密成功,证明消息来自server,进行逻辑处理
如果过程中,解密失败,那么证明消息来源错误。不进行逻辑处理。这样就完成了双向的身份认证。
下面我附上简单的SSLServer.java SSLClient.java,供大家演示用。
启动服务端的时候,大家不妨采用telnet 127.0.0.1 7777连接,看看能不能实现消息传递。
ssl demo
受限于自己对jsse理解非常的浅,上面的文章仅仅是覆盖了jsse很表层的内容。
推荐ibm网站上的一篇文章,对jsse和ssl写得很深入浅出。
为高级 JSSE 开发人员定制 :http://www.ibm.com/developerworks/cn/java/j-customssl/
JSSE(Java 安全套接字扩展,Java Secure Socket Extension)使 Java 应用程序能够在因特网上使用 SSL 安全地进行通信。由于 developerWorks 已经提供了一篇涵盖 JSSE 基本用法的教程(请参阅参考资料),所以本文将集中阐述该技术的更高级用法。本文将演示如何使用 JSSE 接口定制 SSL 连接的属性。
首先,我们将开发一个非常简单的安全客户机/服务器聊天应用程序。在我们构建该程序的客户机端时,我将演示如何定制 KeyStore 和 TrustStore 文件,以便从客户机的文件系统装入它们。接着,我们将着重说明证书和标识。通过从 KeyStore 选择不同的证书,可以将客户机以不同的形式提供给不同的服务器。如果您的客户机应用程序需要连接到多个对等方,或者甚至它需要冒充不同的用户,这项高级的功能都特别有用。
由于本文着重讲述更高级的主题,因此假定您已经具备了 JSSE 的使用经验。要运行示例,需要一个带有正确安装和配置 JSSE 提供程序的 Java SDK。J2SE 1.4 SDK 提供了已安装和配置的 JSSE。如果您正在使用 J2SE 1.2 或 1.3,则需要获取一个 JSSE 实现并安装它。请参阅参考资料下载 JSSE 扩展。
JSSE API 只是 J2SE 1.4 的一项标准,并且早期的 JSSE 实现之间存在略有不同的变体。本文的示例基于 1.4 API。必要的时候,我会强调使示例与 J2SE 1.2 和 1.3 的 Sun JSSE 实现协同工作所必需的更改。
在我们深入研究 JSSE 之前,先让我们熟悉一下将要使用的客户机/服务器应用程序。SimpleSSLServer 和 SimpleSSLClient 是我们的演示应用程序的两个组件。为了运行示例,需要在应用程序的每一端上设置好几个 KeyStore 和 TrustStore 文件。特别是您将需要:
接下来,下载本文随附的 jar 文件。这些文件包含客户机/服务器应用程序的源代码和已编译的版本,因此,只要把它们放到 CLASSPATH 中,就可以使用了。
要运行 SimpleSSLServer,我们输入如下(稍微冗长的)命令:
java -Djavax.net.ssl.keyStore=serverKeys -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStore=serverTrust -Djavax.net.ssl.trustStorePassword=password SimpleSSLServer |
可以看到,我们已指定了 KeyStore,用它来标识服务器,还指定了在 KeyStore 中设置的密码。由于服务器将需要客户机认证,因此我们也为它提供了 TrustStore。通过指定 TrustStore,我们确保 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 上的服务器。可以在命令行上使用 -host
和-port
参数更改主机。当客户机已连接到服务器时,您会得到消息:
Connected |
与此同时,服务器将报告连接请求,并显示由客户机提供的区别名,如下所示:
1: New connection request 1: Request from CN=Bob, OU=developerWorks, O=IBM, L=Winchester, ST=Hampshire, C=UK |
为了测试新连接,试着向客户机输入一些文本,按 Return 键,并观察服务器是否回显文本。要杀死客户机,在客户机控制台上按 Ctrl-C 键。服务器将如下所示记录断开连接:
1: Client disconnected |
无需杀死服务器;在各种练习过程中我们只需保持服务器运行。
尽管本文余下部分主要都是讲述客户机应用程序的,但是,查看一下服务器代码仍然是很值得的。除了可以了解服务器应用程序是如何工作外,您还可以学会如何使用 HandshakeCompletedListener
接口检索有关 SSL 连接的信息。
SimpleSSLServer.java
从三条 import 语句开始,如下所示:
import javax.net.ssl.*; import java.security.cert.*; import java.io.*; |
javax.net.ssl
是三条语句中最重要的;它包含大多数核心 JSSE 类,我们要用它处理任何和 SSL 有关的工作。java.security.cert
在您需要操作单独的证书(在本文后面我们将这样做)时很有用。java.io
是标准的 Java I/O 包。在本案例中,我们将使用它来处理通过安全套接字接收和发送的数据。接下来, main()
方法检查命令行中可选的 -port
参数。然后它获取缺省的 SSLServerSocketFactory
,构造一个SimpleSSLServer
对象,把工厂(factory)传递给构造器,并且启动服务器,如下所示:
SSLServerSocketFactory ssf= (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SimpleSSLServer server=new SimpleSSLServer(ssf, port); server.start(); |
由于服务器是在单独的线程上运行的,因此只要启动并运行, main()
就退出。新的线程调用run()
方法,这样会创建一个 SSLServerSocket
,并且设置服务器以要求客户机认证,如下所示:
SSLServerSocket serverSocket= (SSLServerSocket)serverSocketFactory.createServerSocket(port); serverSocket.setNeedClientAuth(true); |
HandshakeCompletedListener
将它激活之后, run()
方法进行无限循环,等待来自客户机的请求。每个套接字都与HandshakeCompletedListener
实现相关联,后者用来显示来自客户机证书的区别名(DN)。套接字的InputStream
封装在一个 InputDisplayer
中,它作为另一个线程运行,并且将来自套接字的数据回显到System.out
。SimpleSSLServer 的主循环如清单 1 所示:
清单 1. SimpleSSLServer 主循环
while (true) { String ident=String.valueOf(id++); // Wait for a connection request. SSLSocket socket=(SSLSocket)serverSocket.accept(); // We add in a HandshakeCompletedListener, which allows us to // peek at the certificate provided by the client. HandshakeCompletedListener hcl=new SimpleHandshakeListener(ident); socket.addHandshakeCompletedListener(hcl); InputStream in=socket.getInputStream(); new InputDisplayer(ident, in); } |
我们的 HandshakeCompletedListener
―SimpleHandshakeListener
提供了一个 handshakeCompleted()
方法的实现。当 SSL 握手阶段完成时,该方法由 JSSE 基础设施调用,并且传递(在HandshakeCompletedEvent
对象中)有关连接的信息。我们使用这个方法获取并显示客户机的 DN,如清单 2 所示:
清单 2. SimpleHandshakeListener
class SimpleHandshakeListener implements HandshakeCompletedListener { String ident; /** * Constructs a SimpleHandshakeListener with the given * identifier. * @param ident Used to identify output from this Listener. */ public SimpleHandshakeListener(String ident) { this.ident=ident; } /** Invoked upon SSL handshake completion. */ public void handshakeCompleted(HandshakeCompletedEvent event) { // Display the peer specified in the certificate. 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
返回作为X509Certificate
对象数组的证书链。这些证书对象建立对等方的(即客户机的)标识。数组中的第一个是客户机本身的证书;最后一个通常是 CA 证书。一旦我们拥有了对等方的证书,我们可以获取 DN 并将其显示到System.out
。 X509Certificate
是在包java.security.cert
中定义的。
我们将研究的第一个客户机应用程序根本不能做什么。但是,在后面的示例中我们会扩展它来阐述更高级的功能。设置 SimpleSSLClient 的目的是为了方便地添加子类。打算覆盖下面四个方法:
main()
当然是在从命令行运行类时被调用。对于每个子类,main()
必须构造一个合适类的对象,并调用对象上的 runClient()
和 close()
方法。这些方法是在超类 ―SimpleSSLClient
上提供的,并且不打算被覆盖。 handleCommandLineOption()
和 displayUsage()
允许每个子类在命令行上添加选项,而无需更新父类。它们都从runClient()
方法调用。 getSSLSocketFactory()
是一个有趣的方法。JSSE 安全套接字始终是从SSLSocketFactory
对象构造的。通过构造一个定制的套接字工厂,我们可以定制 JSSE 的行为。为了将来练习的目的,每个 SimpleSSLClient 子类都实现该方法,并相应定制SSLSocketFactory
。 目前,SimpleSSLClient 仅能理解 -host
和-port
参数,这允许用户把客户机指向服务器。在这第一个基本示例中,getSSLSocketFactory
返回(JVM 范围的)缺省工厂,如下所示:
protected SSLSocketFactory getSSLSocketFactory() throws IOException, GeneralSecurityException { return (SSLSocketFactory)SSLSocketFactory.getDefault(); } |
从子类的 main()
方法调用的 runClient()
方法,负责处理命令行参数,然后从子类获取SSLSocketFactory
来使用。然后它使用 connect()
方法连接到服务器,并且使用 transmit()
方法在安全通道上开始传输数据。
connect()
方法相当简单。在使用 SSLSocketFactory
连接到服务器之后,它调用安全套接字上的startHandshake
。这迫使 JSSE 完成 SSL 握手阶段,并因而触发服务器端上的HandshakeCompletedListener
。尽管 JSSE 确实会自动启动握手,但是仅当数据首次通过套接字发送时它才这样做。因为用户在键盘上输入消息之前我们不会发送任何数据,但是我们希望服务器立即报告连接,所以我们需要使用startHandshake
强制进行握手。
transmit()
方法同样相当简单。它的首要任务把输入源包装到适当的Reader
,如下所示:
BufferedReader reader=new BufferedReader( new InputStreamReader(in)); |
我们使用 BufferedReader
,因为它将帮我们把输入分割成单独的行。
接下来, transmit()
方法把输出流 ― 在本案例中,由安全套接字提供OutputStream
― 包装到适当的 Writer
中。服务器希望文本是以 UTF-8 编码的,因此我们可以让OutputStreamWriter
使用下列编码:
writer=new OutputStreamWriter(socket.getOutputStream(), "UTF-8"); |
主循环很简单;正如您在清单 3 中看到的,它看起来更象 SimpleSSLServer 中 InputDisplayer
的主循环:
清单 3. SimpleSSLClient 主循环
boolean done=false; while (!done) { String line=reader.readLine(); if (line!=null) { writer.write(line); writer.write('\n'); writer.flush(); } else done=true; } |
基本的 JSSE 服务器和客户机代码就只有这些。现在,我们可以继续扩展 SimpleSSLClient,并且看看一些其它 getSSLSocketFactory
实现。
自制的 KeyStore
还记得我们是如何运行 SimpleSSLClient 的吗?命令如下:
java -Djavax.net.ssl.keyStore=clientKeys -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStore=clientTrust -Djavax.net.ssl.trustStorePassword=password SimpleSSLClient |
命令简直太长了!幸运的是,该示例及接下来的示例将为您演示如何设置一个带有到 KeyStore 和 TrustStore 的硬编码路径的 SSLSocketFactory
。除了减少上述命令的长度之外,您将学习的技术将允许您设置多个SSLSocketFactory
对象,每个对象都带有不同的 KeyStore 和 TrustStore 设置。如果没有这种配置,JVM 中的每个安全连接必须使用相同的 KeyStore 和 TrustStore。尽管对于较小的应用程序而言这是可以接受的,但是较大的应用程序可能需要连接到多个代表许多不同用户的对等方。
介绍 CustomKeyStoreClient
对于第一个示例,我们将使用示例应用程序 CustomKeyStoreClient(可在本文的源代码中找到)来动态定义一个 KeyStore。在研究源代码之前,让我们看看正在使用的 CustomKeyStoreClient。对于这个练习,我们将指定 TrustStore 而不是 KeyStore。在 CustomKeyStoreClient 命令行上输入下列参数,我们将看到出现的结果:
java -Djavax.net.ssl.trustStore=clientTrust -Djavax.net.ssl.trustStorePassword=password CustomKeyStoreClient |
假定客户机连接良好,服务器将报告说提供的证书是有效的。连接成功,因为 CustomKeyStoreClient.java
已经硬编码了 KeyStore 的名称(clientKeys
)和密码( password
)。如果您为客户机 KeyStore 选择了另外的文件名或密码,那么可以使用新的命令行选项-ks
和 -kspass
来指定它们。
研究一下 CustomKeystoreClient.java
的源代码,getSSLSocketFactory
做的第一件事是调用助手方法getKeyManagers()
。稍后我们将考虑这是如何工作的;目前只是注明它返回KeyManager
对象数组,已经利用必需的 KeyStore 文件和密码对其进行了设置。
清单 4. CustomKeyStoreClient.getSSLSocketFactory
protected SSLSocketFactory getSSLSocketFactory() throws IOException, GeneralSecurityException { // Call getKeyManagers to get suitable key managers KeyManager[] kms=getKeyManagers(); // Now construct a SSLContext using these KeyManagers. We // specify a null TrustManager and SecureRandom, indicating that the // defaults should be used. SSLContext context=SSLContext.getInstance("SSL"); context.init(kms, null, null); // Finally, we get a SocketFactory, and pass it to SimpleSSLClient. SSLSocketFactory ssf=context.getSocketFactory(); return ssf; } |
获得 KeyManager
数组之后, getSSLSocketFactory
执行一些对所有 JSSE 定制通常都很重要的设置工作。为了构造SSLSocketFactory
,应用程序获取一个 SSLContext
实例,对其进行初始化,然后使用 SSLContext
生成一个SSLSocketFactory
。
当得到 SSLContext
时,我们指定 "SSL"
的协议;我们也可以在这放入特定的 SSL(或 TLS)协议版本,并且强制通信在特定的级别发生。通过指定"SSL"
,我们允许 JSSE 缺省至它能支持的最高级别。
SSLContext.init
的第一个参数是要使用的 KeyManager
数组。第二个参数(这里保留为 null)类似于 TrustManager
对象数组,稍后我们将使用它们。通过让第二个参数为 null,我们告诉 JSSE 使用缺省的 TrustStore,它从javax.net.ssl.trustStore
和 javax.net.ssl.trustStorePassword
系统属性挑选设置。第三个参数允许我们覆盖 JSSE 的随机数生成器(RNG)。RNG 是 SSL 的一个敏感领域,误用该参数会致使连接变得不安全。我们让该参数为 null,这样允许 JSSE 使用缺省的 ― 并且安全的!―SecureRandom
对象。
装入 KeyStore
接下来,我们将研究 getKeyManagers
如何装入和初始化KeyManagers
数组。先从清单 5 中的代码开始,然后我们将讨论正在发生什么。
清单 5. 装入和初始化 KeyManagers
protected KeyManager[] getKeyManagers() throws IOException, GeneralSecurityException { // First, get the default KeyManagerFactory. String alg=KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg); // Next, set up the KeyStore to use. We need to load the file into // a KeyStore instance. FileInputStream fis=new FileInputStream(keyStore); KeyStore ks=KeyStore.getInstance("jks"); ks.load(fis, keyStorePassword.toCharArray()); fis.close(); // Now we initialize the TrustManagerFactory with this KeyStore kmFact.init(ks, keyStorePassword.toCharArray()); // And now get the TrustManagers KeyManager[] kms=kmFact.getKeyManagers(); return kms; } |
首要工作是获取 KeyManagerFactory
,但是要这样做,我们需要知道将使用哪种算法。幸运的是,JSSE 使缺省的KeyManagerFactory
算法可用。可以使用 ssl.KeyManagerFactory.algorithm
安全性属性配置缺省算法。
接下来, getKeyManagers()
方法装入 KeyStore 文件。这其中包括从文件建立一个InputStream
、获取一个 KeyStore
实例,以及从InputStream
装入 KeyStore
。除了InputStream
, KeyStore
需要知道流的格式(我们使用缺省的"jks"
)和存储密码。存储密码必须作为字符数组提供。
要说明的一个可能很有用的窍门是, KeyStore.load
会获取任何InputStream
。您的应用程序可以从任何地方构建这些流;除了文件,您可以通过网络、从移动设备获取流,或者甚至直接生成流。
装入 KeyStore
之后,我们使用它来初始化以前创建的KeyManagerFactory
。我们需要再次指定一个密码,这次是单独的证书密码。通常,对于 JSSE 而言,KeyStore 中的每个证书都需要具备与 KeyStore 本身相同的密码。自己构造KeyManagerFactory
可以克服这个限制。
KeyManagerFactory
初始化之后,它通常使用getKeyManagers()
方法获取相应的 KeyManager
对象的数组。
对于 CustomKeyStoreClient 而言,我们已经研究了如何从任意的位置(本文使用本地文件系统)装入 KeyStore,以及如何让证书和 KeyStore 本身使用不同的密码。稍后我们将研究如何允许 KeyStore 中的每个证书拥有不同的密码。尽管在本示例中我们着重于客户机端,但是,我们可以在服务器端使用相同的技术来构建适当的SSLServerSocketFactory
对象。
使用您自己的 TrustStore
覆盖 JSSE 的缺省 TrustStore 非常类似于我们刚才用 KeyStore 所做的工作,这并不令人惊奇。我们已经设置了 CustomTrustStoreClient(可在本文的源代码中找到)来使用硬编码的 KeyStore 和硬编码的 TrustStore。开始运行它所需做的全部工作就是输入命令:java CustomTrustStoreClient
。
CustomTrustStoreClient 希望 KeyStore 将是一个名为 clientKeys
并且密码为password
的文件。希望 TrustStore 将是一个名为clientTrust
,密码为 password
的文件。就象使用 CustomKeyStoreClient 一样,可以使用-ks
、 -kspass
、-ts
和 -tspass
参数覆盖这些缺省值。
getSSLSocketFactory()
在许多方面与 CustomKeyStoreClient 中相同方法是一样的。我们甚至从 CustomKeyStoreClient 调用getKeyManagers()
方法来获取与前面示例中相同的定制的KeyManager
对象数组。但是这时 getSSLSocketFactory
还必须获取一个定制的TrustManager
对象数组。在清单 6 中,我们可以看到getSSLSocketFactory
如何使用助手方法 getTrustManagers()
获取定制的 TrustManager
对象:
清单 6. getSSLSocketFactory 如何使用 TrustManagers
protected SSLSocketFactory getSSLSocketFactory() throws IOException, GeneralSecurityException { // Call getTrustManagers to get suitable trust managers TrustManager[] tms=getTrustManagers(); // Call getKeyManagers (from CustomKeyStoreClient) to get suitable // key managers KeyManager[] kms=getKeyManagers(); // Next construct and initialize a SSLContext with the KeyStore and // the TrustStore. We use the default SecureRandom. SSLContext context=SSLContext.getInstance("SSL"); context.init(kms, tms, null); // Finally, we get a SocketFactory, and pass it to SimpleSSLClient. SSLSocketFactory ssf=context.getSocketFactory(); return ssf; } |
这时,当初始化上下文时,我们覆盖了 KeyStore 和 TrustStore。但是,我们仍然让 JSSE 通过传递 null
作为第三个参数来使用它缺省的SecureRandom
。
getTrustManagers
也非常类似于 CustomKeyStoreClient 的等价物同样不足为奇,如清单 7 所示:
清单 7. 装入和初始化 TrustManagers
protected TrustManager[] getTrustManagers() throws IOException, GeneralSecurityException { // First, get the default TrustManagerFactory. String alg=TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmFact=TrustManagerFactory.getInstance(alg); // Next, set up the TrustStore to use. We need to load the file into // a KeyStore instance. FileInputStream fis=new FileInputStream(trustStore); KeyStore ks=KeyStore.getInstance("jks"); ks.load(fis, trustStorePassword.toCharArray()); fis.close(); // Now we initialize the TrustManagerFactory with this KeyStore tmFact.init(ks); // And now get the TrustManagers TrustManager[] tms=tmFact.getTrustManagers(); return tms; } |
就象以前一样, getTrustManagers()
方法首先根据缺省算法实例化一个TrustManagerFactory
。然后将 TrustStore 文件装入KeyStore
对象 ― 是的,命名不大恰当 ― 并且初始化 TrustManagerFactory
。
跟 KeyStore 等价物不同,请注意,当初始化 TrustManagerFactory
时,无需提供密码。不象私钥,可信的证书无需利用单独的密码进行保护。
到目前为止,我们已经研究了如何动态地覆盖 KeyStore 和 TrustStore。到这两个示例都完成时,您应该非常清楚如何设置 KeyManagerFactory
和TrustManagerFactory
,并使用这些来“播种”一个SSLContext
。最后的示例有点烦琐:我们将构建自己的 KeyManager
实现。
定制 KeyManager 设置:选择别名
当运行客户机应用程序的以前版本时,您是否注意到了服务器显示的是哪个证书 DN?我们故意设置客户机 KeyStore 以获得两个可接受的证书,一个用于 Alice,另一个用于 Bob。在这个案例中,JSSE 将选择任何一个它认为合适的证书。在我的安装中,似乎始终选取 Bob 的证书,但是您的 JSSE 的行为可能有所不同。
我们的示例应用程序 ― SelectAliasClient 允许您选择提供哪个证书。因为我们在 Keystore 中按照别名 alice
或bob
命名了每个证书,所以要选择 Alice 的证书可输入命令:java SelectAliasClient -alias alice
。
当客户机连接并且 SSL 握手完成时,服务器将用如下所示进行响应:
1: New connection request 1: Request from CN=Alice, OU=developerWorks, O=IBM, L=Winchester, ST=Hampshire, C=UK |
(或者创建 Alice 的证书时所选的任何值)。类似地,如果选择 Bob 的证书,请输入: java SelectAliasClient -alias bob
,服务器将报告下述信息:
2: New connection request 2: Request from CN=Bob, OU=developerWorks, O=IBM, L=Winchester, ST=Hampshire, C=UK |
定制 KeyManager 实现
为了强制选择一个特殊的别名,我们将编写一个 X509KeyManager
实现,KeyManager
通常由 JSSE 使用来进行 SSL 通信。我们的实现将包含一个真正的X509KeyManager
,并且简单地通过它传递大多数的调用。它拦截的唯一方法是chooseClientAlias()
;我们的实现检查以便了解所需的别名有效还是无效,如果有效,则返回它。
在 SSL 握手阶段, X509KeyManager
接口使用许多方法来检索密钥,然后使用它来标识对等方。在参考资料部分可以找到所有方法的参考。下列方法对于本练习很重要:
getClientAliases()
和 getServerAliases()
分别为使用SSLSocket
和 SSLServerSocket
提供了有效的别名数组。chooseClientAlias()
和 chooseServerAlias()
返回单个有效的别名。 getCertificateChain()
和 getPrivateKey()
每个都把别名作为参数,并返回有关已标识证书的信息。定制 KeyManager 中的控制流
控制流的工作如下所示:
chooseClientAlias
以发现要使用的别名。chooseClientAlias
在真实的 X509KeyManager
上调用 getClientAliases
来发现一个有效的别名列表,以便于它能检查所需的别名是否有效。X509KeyManager
的getCertificateChain
和 getPrivateKey
,X509KeyManager 让调用可以访问被包装的 KeyManager。KeyManager AliasForcingKeyManager()
的chooseClientAlias()
方法实际上需要多次调用 getClientAliases()
,一次对应一个 JSSE 安装支持的密钥类型,如清单 8 所示:
清单 8. 强制别名的选择
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { // For each keyType, call getClientAliases on the base KeyManager // to find valid aliases. If our requested alias is found, select it // for return. boolean aliasFound=false; for (int i=0; i |
AliasForcingKeyManager
需要 X509KeyManager
的其它五种方法的实现;这些只是调用它们在 baseKM
上的对应部分。
目前,它仍然使用 AliasForcingKeyManager
,而不是通常的KeyManager
。这发生在 getSSLSocketFactory
中,它首先从其它示例中调用getKeyManagers
和 getTrustManagers
,但是接着将每个从getKeyManagers
返回的 KeyManager
封装进一个AliasForcingKeyManager
实例,如清单 9 所示:
清单 9. 封装 X509KeyManagers
protected SSLSocketFactory getSSLSocketFactory() throws IOException, GeneralSecurityException { // Call the superclasses to get suitable trust and key managers KeyManager[] kms=getKeyManagers(); TrustManager[] tms=getTrustManagers(); // If the alias has been specified, wrap recognized KeyManagers // in AliasForcingKeyManager instances. if (alias!=null) { for (int i=0; i |
可以使用本文探讨的技术覆盖 KeyManager
的任何方面。类似地,可以使用它们代替TrustManager
,更改 JSSE 的机制以决定是否信任从远程对等方流出的证书。
结束语
本文已经讨论了相当多的技巧和技术,因此让我们以快速回顾来结束本文。现在您应当基本了解如何:
HandshakeCompletedListener
收集有关连接的信息SSLContext
获取 SSLSocketFactory
在适当的地方,我还建议扩展这些技术以用于各种应用程序案例。在您自己的实现中封装 X509KeyManager
的技巧可用于 JSSE 中的许多其它类,当然,利用TrustStore
和 KeyStore
可以做更有趣的事情,而不只是装入硬编码的文件。
不管您如何选择实现本文演示的高级 JSSE 定制,任何一个都不是随便就可以实现的。在调整 SSL 内部机理的时候,请牢记:一个错误就会致使您连接变得不安全,这很重要。
使用 JSSE 定制 SSL 连接的属性
简介: 当数据在网络上传播的时候,通过使用 SSL 对其进行加密和保护,JSSE 为 Java 应用程序提供了安全的通信。在本篇有关该技术的高级研究中,Java 中间件开发人员 Ian Parkinson 深入研究了 JSSE API 较不为人知的方面,为您演示了如何围绕 SSL 的一些限制进行编程。您将学习如何动态地选择 KeyStore 和 TrustStore、放宽 JSSE 的密码匹配要求,以及构建您自己定制的 KeyManager 实现。
参考资料
HttpsUrlConnection
类通过防火墙建立 HTTPS 通道。X509KeyManager
的 Sun javadoc,获取所有方法的完整讨论。