Java中使用JSSE实现SSL/TLS安全协议

一、简介

SSL/TLS协议是安全的通信模式,而对于这些底层协议,如果要每个开发者都自己去实现显然会带来不必要的麻烦,正是为了解决这个问题Java为广大开发者提供了Java安全套接字扩展——JSSE,它包含了实现Internet安全通信的一系列包的集合,是SSL和TLS的纯Java实现,同时它是一个开放的标准,每个公司都可以自己实现JSSE,通过它可以透明地提供数据加密、服务器认证、信息完整性等功能,就像使用普通的套接字一样使用安全套接字,大大减轻了开发者的负担,使开发者可以很轻松将SSL协议整合到程序中,并且JSSE能将安全隐患降到了最低点。

在用JSSE实现SSL通信过程中主要会遇到以下类和接口,由于过程中涉及到加解密、**生成等运算的框架和实现,所以也会间接用到JCE包的一些类。如图3-1-7-2为JSSE接口的主要类图:

Java中使用JSSE实现SSL/TLS安全协议

① 通信核心类——SSLSocket和SSLServerSocket。它们对应的就是Socket与ServerSocket,只是表示实现了SSL协议的Socket,ServerSocket,同时它们也是Socket与ServerSocket的子类。SSLSocket负责的事情包括【设置加密套件、管理SSL会话、处理握手结束时间、设置客户端模式或服务器模式】。
② 客户端与服务器端Socket工厂——SSLSocketFactory和SSLServerSocketFactory。在设计模式中工厂模式是专门用于生产出需要的实例,这里也是把SSLSocket、SSLServerSocket对象创建的工作交给这两个工厂类。
③ SSL会话——SSLSession。安全通信握手过程需要一个会话,为了提高通信的效率,SSL协议允许多个SSLSocket共享同一个SSL会话,在同一个会话中,只有第一个打开的SSLSocket需要进行SSL握手,负责生成**及交换**,其余SSLSocket都共享**信息。
④ SSL上下文——SSLContext。它是对整个SSL/TLS协议的封装,表示了安全套接字协议的实现。主要负责设置安全通信过程中的各种信息,例如跟证书相关的信息。并且负责构建SSLSocketFactory、SSLServerSocketFactory和SSLEngine等工厂类。
⑤ SSL非阻塞引擎——SSLEngine。假如你要进行NIO通信,那么将使用这个类,它让通过过程支持非阻塞的安全通信。
⑥ **管理器——KeyManager。此接口负责选择用于证实自己身份的安全证书,发给通信另一方。KeyManager对象由KeyManagerFactory工厂类生成。
⑦ 信任管理器——TrustManager。此接口负责判断决定是否信任对方的安全证书,TrustManager对象由TrustManagerFactory工厂类生成。
⑧ **证书存储设施——KeyStore。这个对象用于存放安全证书,安全证书一般以文件形式存放,KeyStore负责将证书加载到内存。
通过上面这些类就可以完成SSL协议的安全通信了,在利用SSL/TLS进行安全通信时,客户端跟服务器端都必须要支持SSL/TLS协议,不然将无法进行通信。而且客户端和服务器端都可能要设置用于证实自己身份的安全证书,并且还要设置信任对方的哪些安全证书。

关于身份认证方面有个名词叫客户端模式,一般情况客户端要对服务器端的身份进行验证,但是无需向服务器证实自己的身份,这样不用向对方证实自己身份的通信端我们就说它处于客户模式,否则成它处于服务器模式。SSLSocket的setUseClientMode(Boolean mode)方法可以设置客户端模式或服务器模式。
 

二、证书简介

看一个用JSSE简单实现SSL通信的例子。
① 解决证书问题。
一般而言作为服务器端必须要有证书以证明这个服务器的身份,并且证书应该描述此服务器所有者的一些基本信息,例如公司名称、联系人名等。证书由所有人以密码形式签名,基本不可伪造,证书获取的途径有两个:一是从权威机构购买证书,权威机构担保它发出的证书的真实性,而且这个权威机构被大家所信任,进而你可以相信这个证书的有效性;另外一个是自己用JDK提供的工具keytool创建一个自我签名的证书,这种情况下一般是我只想要保证数据的安全性与完整性,避免数据在传送的过程中被窃听或篡改,此时身份的认证已不重要,重点已经在端与端传输的秘密性上,证书的作用只体现在加解密签名。
另外,关于证书的一些概念在这里陈述,一个证书是一个实体的数字签名,这个实体可以是一个人、一个组织、一个程序、一个公司、一个银行,同时证书还包含这个实体的公共钥匙,此公共钥匙是这个实体的数字关联,让所有想同这个实体发生信任关系的其他实体用来检验签名。而这个实体的数字签名是实体信息用实体的私钥加密后的数据,这条数据可以用这个实体的公共钥匙解密,进而鉴别实体的身份。这里用到的核心算法是非对称加密算法。

 

SSL协议通信涉及**储存的文件格式比较多,很容易搞混,例如xxx.cer、xxx.pfx、xxx.jks、xxx.keystore、xxx.truststore等格式文件。如图3-1-7-3,搞清楚他们有助于理解后面的程序,.cer格式文件俗称证书,但这个证书中没有私钥,只包含了公钥;.pfx格式文件也称为证书,它一般供浏览器使用,而且它不仅包含了公钥,还包含了私钥,当然这个私钥是加密的,不输入密码是解不了密的;.jks格式文件表示java**存储器(javakey store),它可以同时容纳N个公钥跟私钥,是一个**库;.keystore格式文件其实跟.jks基本是一样的,只是不同公司叫法不太一样,默认生成的证书存储库格式;.truststore格式文件表示信任证书存储库,它仅仅包含了通信对方的公钥,当然你可以直接把通信对方的jks作为信任库(就算如此你也只能知道通信对方的公钥,要知道**都是加密的,你无从获取,只要算法不被**)。有些时候我们需要把pfx或cert转化为jks以便于用java进行ssl通信,例如一个银行只提供了pfx证书,而我们想用java进行ssl通信时就要将pfx转化为jks格式。

Java中使用JSSE实现SSL/TLS安全协议

 

 

三、生成证书,

按照理论上,我们一共需要准备四个文件,两个keystore文件和两个truststore文件,通信双方分别拥有一个keystore和一个truststore,keystore用于存放自己的**和公钥,truststore用于存放所有需要信任方的公钥。这里为了方便直接使用jks即keystore替代truststore(免去证书导来导去),因为对方的keystore包含了自己需要的信任公钥。
下面使用jdk自带的工具分别生成服务器端证书,通过如下命令并输入姓名、组织单位名称、组织名称、城市、省份、国家信息即可生成证书密码为myserver的证书,此证书存放在密码也为123456的myserver.jks证书存储库中。如果你继续创建证书将继续往myserver.jks证书存储库中添加证书。

 

keytool用法:

1.生成

 keytool -genkey -alias yushan(别名) -keypass yushan(别名密码) -keyalg RSA(算法) -keysize 1024(**长度) -validity 365(有效期,天单位) -keystore e:\yushan.keystore(指定生成证书的位置和证书名称) -storepass 123456(获取keystore信息的密码);回车输入相关信息即可;

2.导出(证书库导出到crt证书文件)

keytool -export -alias yushan -keystore e:\yushan.keystore -file e:\yushan.crt(指定导出的证书位置及证书名称) -storepass 123456

3.导入(从证书文件导入到keystore或jks文件)

准备一个导入的证书:
keytool -genkey -alias shuany -keypass shuany -keyalg RSA -keysize 1024 -validity 365 -keystore  e:\shuany.keystore -storepass 123456 -dname "CN=shuany, OU=xx, O=xx, L=xx, ST=xx, C=xx";
keytool -export -alias shuany -keystore e:\shuany.keystore -file e:\shuany.crt -storepass 123456
 现在将shuany.crt 加入到yushan.keystore中:keytool -import -alias shuany(指定导入证书的别名,如果不指定默认为mykey,别名唯一,否则导入出错) -file e:\shuany.crt -keystore e:\yushan.keystore -storepass 123456 keytool -list  -v -keystore e:\keytool\yushan.keystore -storepass 123456

 

制作我的证书:

服务器端:

Java中使用JSSE实现SSL/TLS安全协议

客户端:

Java中使用JSSE实现SSL/TLS安全协议

 

生成文件:

Java中使用JSSE实现SSL/TLS安全协议

 

 

四、实现SSL

服务端简单实现:

 

 
  1. public static void main(String[] args) throws Exception {

  2.  
  3. //**管理器

  4. KeyStore serverKeyStore = KeyStore.getInstance("JKS");//证书库格式

  5. serverKeyStore.load(new FileInputStream("e:\\myserver.jks"), "123456".toCharArray());//加载**库

  6.  
  7. KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");//证书格式

  8. kmf.init(serverKeyStore, "123456".toCharArray());//加载**储存器

  9.  
  10. //信任管理器

  11. KeyStore clientKeyStore = KeyStore.getInstance("JKS");

  12. clientKeyStore.load(new FileInputStream("e:\\myclient.jks"), "123456".toCharArray());

  13.  
  14. TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

  15. tmf.init(clientKeyStore);

  16.  
  17. //SSL上下文设置

  18. SSLContext sslContext = SSLContext.getInstance("SSL");

  19. sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

  20.  
  21. //SSLServerSocket

  22. SSLServerSocketFactory serverFactory = sslContext.getServerSocketFactory();

  23. SSLServerSocket svrSocket = (SSLServerSocket) serverFactory.createServerSocket(34567);

  24. //svrSocket.setNeedClientAuth(true);//客户端模式,服务端需要验证客户端身份

  25.  
  26. String[] supported = svrSocket.getEnabledCipherSuites();//加密套件

  27. svrSocket.setEnabledCipherSuites(supported);

  28.  
  29. //接收消息

  30. System.out.println("端口已打开,准备接受信息");

  31.  
  32. SSLSocket cntSocket = (SSLSocket) svrSocket.accept();//开始接收

  33. InputStream in=cntSocket.getInputStream();//输入流

  34. int a=in.read(new byte[102]);

  35. //循环检查是否有消息到达

  36. System.out.println("来自于客户端:" + a);

  37.  
  38.  
  39. }

 

基本顺序是先得到一个SSLContext实例,再对SSLContext实例进行初始化,**管理器及信任管理器作为参数传入,证书管理器及信任管理器按照指定的**存储器路径和密码进行加载。接着设置支持的加密套件,最后让SSLServerSocket开始监听客户端发送过来的消息。

 

客户端实现:

 
  1. public static void main(String[] args) throws Exception {

  2.  
  3. //**管理器

  4. KeyStore clientKeyStore = KeyStore.getInstance("JKS");

  5. clientKeyStore.load(new FileInputStream("e:\\myclient.jks"), "123456".toCharArray());

  6.  
  7. KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");

  8. kmf.init(clientKeyStore, "123456".toCharArray());

  9.  
  10.  
  11. //信任管理器

  12. KeyStore serverKeyStore = KeyStore.getInstance("JKS");

  13. serverKeyStore.load(new FileInputStream("e:\\myserver.jks"), "123456".toCharArray());

  14.  
  15.  
  16. TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

  17. tmf.init(serverKeyStore);

  18.  
  19. //SSL上下文

  20. SSLContext sslContext = SSLContext.getInstance("SSL");

  21. sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

  22.  
  23.  
  24. SSLSocketFactory sslcntFactory =(SSLSocketFactory) sslContext.getSocketFactory();

  25. SSLSocket sslSocket= (SSLSocket) sslcntFactory.createSocket("127.0.0.1", 34567);

  26.  
  27. String[] supported = sslSocket.getSupportedCipherSuites();

  28. sslSocket.setEnabledCipherSuites(supported);

  29.  
  30. //发送

  31. OutputStream out=sslSocket.getOutputStream();

  32. out.write("hello".getBytes());

  33.  
  34. }


客户端的前面操作基本跟服务器端的一样,先创建一个SSLContext实例,再用**管理器及信任管理器对SSLContext进行初始化,当然这里**存储的路径是指向客户端的client.jks。接着设置加密套件,最后使用SSLSocket进行通信。


注意服务器端有行代码svrSocket.setNeedClientAuth(true);它是非常重要的一个设置方法,用于设置是否验证客户端的身份。假如我们把它注释掉或设置为false,此时客户端将不再需要自己的**管理器,即服务器不需要通过client.jks对客户端的身份进行验证,把**管理器直接设置为null也可以跟服务器端进行通信。
 

最后谈谈信任管理器,它的职责是觉得是否信任远端的证书,那么它凭借什么去判断呢?如果不显式设置信任存储器的文件路径,将遵循如下规则:

①如果系统属性javax.net.ssl.truststore指定了truststore文件,那么信任管理器将去jre路径下的lib/security目录寻找这个文件作为信任存储器;

②如果没设置①中的系统属性,则去寻找一个%java_home%/lib/security/jssecacerts文件作为信任存储器;

③如果jssecacerts不存在而cacerts存在,则cacerts作为信任存储器。



至此,一个利用JSSE实现SSL协议通信的例子已完成。