副标题#e#
概述
操作 Java 的 JSSE(Java Secure Socket Extension)技能,我们可以利便的编写安详套接字措施,关于 JSSE 的先容,可以参阅 Oracle网站提供的 JSSE指导。措施编写进程中,我们需要将数字证书应用到代码中。凡是在正式的产物开拓中,我们可以付出必然的用度,向正规认证机构,譬喻:Verisign、Geotrust、Thawte等申请。
假如只是为了尝试,我们还可以利用 Java 自带的 keytool 东西来建造证书。keytool 是密钥和证书打点东西,生成的密钥或证书,存放在 jks(Java Key Store) 名目标文件里。从用途上来说,jks 名目标文件常用于:
1) 存储非对称密钥对以及数字证书的证书库;
2) 存储信任证书列表的信任库。
留意:差异版本的 Java 自带的 keytool 呼吁行参数大概会略有差异。对比于 Java6,在 Java7 中 keytool 东西有如下窜改:
-export 选项更名为 -exportcert
-genkey 选项更名为 -genkeypair
-import 选项更名为 – importcert
-keyclone 选项被废弃
-identitydb 选项被废弃
-selfcert 选项被废弃
下面将以 Java7 中的 keytool 为例,对常见的用法举办说明。
利用 keytool 建造证书库以及信任库
生成非对称密钥以及自签发证书
呼吁:keytool -genkeypair -alias TEST_ROOT -keystore test_root.jks
表明:生成一对密钥以及一个自签发证书,个中私钥和证书以别名 TEST_ROOT 存储在 test_root.jks 文件中。
留意:利用上述呼吁时,呼吁行会交互的需要手动填写暗码、CN、OU 等信息。
生成证书请求文件
呼吁:keytool -certreq -file test_server.csr -alias TEST_SERVER -keystore test_server.jks
表明:将别名为 TEST_SERVER 的公钥和一些小我私家书息从 test_server.jks 文件中导出,作为证书请求文件。
签发证书
呼吁:keytool -gencert -infile test_server.csr -outfile test_server.cer -alias TEST_ROOT -keystore TEST_ROOT.jks
表明:利用别名为 TEST_ROOT 的私钥为 test_server.csr 签发证书,并生存到 test_server.cer 文件中。
从 jks 文件中导出证书
呼吁:keytool -exportcert -alias TEST_ROOT -file test_root.cer -keystore test_root.jks
表明:从 test_root.jks 文件中导出别名为 TEST_ROOT 的证书并存放在 test_root.cer 文件中。
导入信任证书到 jks 文件
呼吁:keytool -importcert -alias TEST_ROOT -file test_root.cer -keystore TEST_SERVER.jks
表明:将证书 test_root.cer 以别名 TEST_ROOT 导入 TEST_SERVER.jks 中。
留意:这里的方针 jks 文件里不含有指定的别名,此时的导入条目才会以 trustedCertEntry 信任证书的形式生存。
导入签发证书到 jks 文件 ( 更新证书 )
呼吁:keytool -importcert -alias TEST_SERVER -file test_server.cer -keystore TEST_SERVER.jks
表明:将证书 test_server.cer 更新到已存在别名 TEST_SERVER 的 TEST_SERVER.jks 文件中
留意:这里的呼吁和上述导入信任证书的呼吁在形式上完全一样,但浸染差异。
1. 这里的方针 jks 文件里要含有指定的别名,这样 keytool 东西才会领略呼吁为更新证书,并以 PrivateKeyEntry 的形式生存。
2. 在更新被签发证书之前,必然要先将相应的 CA 证书,导入进 jks 文件,不然会报错“keytool 错误 : java.lang.Exception: 无法从回覆中成立链”。
#p#副标题#e#
打印证书内容
呼吁:keytool – printcert – v – file test_server.cer
表明:将证书 test_server.cer 的内容打印出来
留意:也可以利用 -sslserver ip:port 的参数,直接从网络上打印出某个 ssl server 提供的证书的内容,详情见 参考资料中列出的 keytool 利用辅佐。
显示 jks 文件里的内容
呼吁:keytool – list – v – keystore test_server.jks
表明:显示 test_server.jks 里存储的所有条目
留意:这里会要求提供 jks 文件的暗码,假如不输入,也可以显示出所有条目信息,但会提示“存储在密钥库中的信息的完整性尚未获得验证!”
从 jks 文件删除条目
呼吁:keytool -delete -alias TEST_ROOT -keystore test_server.jks
表明:从 test_server.jks 中删除别名为 TEST_ROOT 的条目
安详套接字措施编写的要领
利用 Java 编写安详套接字措施,可以遵循必然的要领,如图 1 所示,展示了相关的各个类之间的干系。个中 Keystore、KeyManagerFactory、TrustManagerFactory、SSLContext 可以称之为“引擎类”(engine class),对它们指定特定的参数 ( 譬喻:协议、算法等 ),就可以发生切合我们要求的,用于编程的工具实例。
图 1. 相关类之间的干系
(注:图片引自 《 Java Secure Socket Extension (JSSE) Reference Guide 》)
编程的步调可以简朴的小结为以下几步:
1. 利用 Keystore 类将证书库或信任库文件加载进来;
2. 利用 KeyManagerFactory 和加载了证书库的 Keystore 实例,发生 KeyManager 实例数组;
3. 利用 TrustManagerFactory 和加载了信任库的 Keystore 实例,发生 TrustManager 实例数组;
4. 利用 SSLContext 初始化 KeyManager 实例数组和 TrustManager 实例数组,从而设定好通信的情况。
5. 操作 SSLContext 发生的 SSLSocket 或 SSLServerSocket 举办通信。
在编写详细措施之前,我们需要操作前文对 keytool 东西的常识先容,筹备如下 jks 文件:
1. test_root.jks:该文件中存有自签发的证书,用作 CA 来签发证书;
2. test_server_cert.jks:该文件中存有 CA 签名的证书,用于 SSL/TSL 通信的处事端;
3. test_server_trust.jks:该文件中存有信任客户端的证书,用于 SSL/TSL 通信的处事端;
4. test_client_cert.jks:该文件中存有 CA 签名的证书,用于 SSL/TSL 通信的客户端;
5. test_client_trust.jks:该文件中存有信任处事端的证书,用于 SSL/TSL 通信的客户端。
假定每个 jks 文件的暗码都设定为“Testpassw0rd”,都存放在“D:”盘下。
通过系统属性指定证书库和信任库
#p#分页标题#e#
这种编写方法较量简朴直观,可以通过给 JVM 通报参数,可能在代码中利用 System.setProperty() 要领,来指定通信需要的 jks 文件。
处事端措施
要运行如清单 1 所示的措施,可以在呼吁行添加如下虚拟机参数,指定处事端措施要利用的证书库和暗码:
-Djavax.net.ssl.keyStore="D:/test_server_cert.jks"
-Djavax.net.ssl.keyStorePassword="Testpassw0rd"
留意到措施中 setNeedClientAuth(false),暗示不需要验证客户端身份。假如这里配置为 true,则我们这里还需要指定信任库和暗码:
-Djavax.net.ssl.trustStore="D:/test_server_trust.jks"
-Djavax.net.ssl.trustStorePassword="Testpassw0rd"
清单 1. 简朴的 SSL 通信处事端措施
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; public class Simple_SSLServerSocket{ // 界说了监听端标语 private final static int LISTEN_PORT=54321; public static void main(String args[]) throws IOException{ SSLServerSocket serverSocket=null; SSLSocket clientSocket=null; // 利用默认方法获取套接字工场实例 SSLServerSocketFactory ssf=(SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); try{ serverSocket=(SSLServerSocket)ssf.createServerSocket(LISTEN_PORT); // 配置不需要验证客户端身份 serverSocket.setNeedClientAuth(false); System.out.println("SSLServer is listening on "+LISTEN_PORT+" port"); // 轮回监听端口,假如有客户端连入就新开一个线程与之通信 while(true){ // 接管新的客户端毗连 clientSocket=(SSLSocket)serverSocket.accept(); ClientConnection clientConnection=new ClientConnection(clientSocket); // 启动一个新的线程 Thread clientThread=new Thread(clientConnection); System.out.println("Client "+clientThread.getId()+" is connected"); clientThread.run(); } }catch(IOException ioExp){ ioExp.printStackTrace(); }catch(Exception e){ e.printStackTrace(); }finally{ serverSocket.close(); } } } class ClientConnection implements Runnable{ private Socket clientSocket=null; public ClientConnection(SSLSocket sslsocket){ clientSocket=sslsocket; } public void run(){ BufferedReader reader=null; // 将吸收到的来自客户端的文字打印出来 try{ reader=new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); while(true){ String line=reader.readLine(); if(line==null){ System.out.println("Communication end."); break; } System.out.println("Receive message: "+line); } reader.close(); clientSocket.close(); }catch(IOException ioExp){ ioExp.printStackTrace(); }catch(Exception e){ e.printStackTrace(); } } }
客户端措施
对应于清单 1 所示的处事端措施,清单 2 是客户端措施,需要在呼吁行添加如下虚拟机参数,指定信任库和暗码:
-Djavax.net.ssl.trustStore="D:/test_client_trust.jks"
-Djavax.net.ssl.trustStorePassword="Testpassw0rd"
假如处事端措施 setNeedClientAuth(true) 要求验证客户端身份,则我们还需要指定证书库和暗码:
-Djavax.net.ssl.keyStore="D:/test_client_cert.jks"
#p#分页标题#e#
-Djavax.net.ssl.keyStorePassword="Testpassw0rd"
清单 2. 简朴的 SSL 通信客户端措施
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; public class Simple_SSLSocket{ // 界说要毗连的处事器名和端标语 private static final int DEFAULT_PORT=54321; private static final String DEFAULT_HOST="localhost"; public static void main(String args[]){ SSLSocket socket=null; // 利用默认的方法获取工场实例 SSLSocketFactory sf=(SSLSocketFactory)SSLSocketFactory.getDefault(); try{ // 毗连处事端的端口,完成握手进程 socket=(SSLSocket)sf.createSocket(DEFAULT_HOST, DEFAULT_PORT); socket.startHandshake(); System.out.println("Connected to "+DEFAULT_HOST+":"+DEFAULT_PORT+" !"); // 从节制台输入要发送给处事端的文字 BufferedReader reader=new BufferedReader(new InputStreamReader(System.in)); Writer writer=new OutputStreamWriter(socket.getOutputStream()); // 可以重复向处事端发送动静 boolean done=false; while (!done) { System.out.print("Send Message: "); String line=reader.readLine(); if (line!=null) { writer.write(line+"\n"); writer.flush(); }else{ done=true; } } socket.close(); }catch (Exception e) { System.out.println("Connection failed: "+e); try{ socket.close(); }catch(IOException ioe){} socket=null; } } }
通过 SSLContext 指定证书库和信任库
前文描写的,通过系统参数指定证书库和信任库的要领,固然简朴易用,可是缺点也是显而易见的,整个措施的情况都得利用同样的 jks 文件。假如措施里有差异的 SSL/TSL 通信,则需要利用差异的 jks 文件,该怎么做呢?
可以利用 SSLContext 来指定 jks 文件,只需要把清单 3 的代码片断替换到清单 1 的“SSLServerSocketFactory ssf”生成处;把清单 4 的代码片断替换到清单 2 的“SSLSocketFactory sf”生成处,再稍作代码调解即可。
(注:实际上,在利用 SSLSocketFactory.getDefault() 可能 SSLServerSocketFactory.getDefault() 建设套接字的时候,措施内部已经利用了默认的 context,其参数就是通过系统属性指定的 )
清单 3. SSLContext 指定证书库
// 相关的 jks 文件及其暗码界说 private final static String CERT_STORE="D:/test_server_cert.jks"; private final static String CERT_STORE_PASSWORD="Testpassw0rd"; // 载入 jks 文件 FileInputStream f_certStore=new FileInputStream(CERT_STORE); KeyStore ks=KeyStore.getInstance("jks"); ks.load(f_certStore, CERT_STORE_PASSWORD.toCharArray()); f_certStore.close(); // 建设并初始化证书库工场 String alg=KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg); kmFact.init(ks, CERT_STORE_PASSWORD.toCharArray()); KeyManager[] kms=kmFact.getKeyManagers(); // 建设并初始化 SSLContext 实例 SSLContext context=SSLContext.getInstance("SSL"); context.init(kms, null, null); SSLServerSocketFactory ssf=(SSLServerSocketFactory)context.getServerSocketFactory();
清单 4. SSLContext 指定信任库
// 相关的 jks 文件及其暗码界说 private final static String TRUST_STORE="D:/test_client_trust.jks"; private final static String TRUST_STORE_PASSWORD="Testpassw0rd"; // 载入 jks 文件 FileInputStream f_trustStore=new FileInputStream(TRUST_STORE); KeyStore ks=KeyStore.getInstance("jks"); ks.load(f_trustStore, TRUST_STORE_PASSWORD.toCharArray()); f_trustStore.close(); // 建设并初始化信任库工场 String alg=TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmFact=TrustManagerFactory.getInstance(alg); tmFact.init(ks); TrustManager[] tms=tmFact.getTrustManagers(); // 建设并初始化 SSLContext 实例 SSLContext context=SSLContext.getInstance("SSL"); context.init(null, tms, null); SSLSocketFactory sf=context.getSocketFactory();
虽然,假如要在处事端可能客户端同时利用证书库和信任库,可将清单 3 和清单 4 的代码用在同一处 context.init() 的处所。
#p#分页标题#e#
这里需要说明的是,假如 context.init() 的第一个 KeyManager[] 参数为 null,则暗示不提供证书;假如第二个 TrustManager[] 参数为 null,则会寻找系统默认提供的信任库 ( 譬喻:JRE 安装目次下的 lib/security/cacerts)。
利用 X509 证书信任打点器
查察本栏目
X509TrustManager 接口扩展了 TrustManager 接口,利用 TrustManager 接口,我们已经可以在措施中自界说信任库了,但假如对方的证书不在信任库中,则通信会直接宣告失败。
假如但愿能自界说信任库的一些行为 ( 譬喻:检讨对方证书,针对异常做一些处理惩罚 ),我们可以利用 X509TrustManager 接口,实现本身的要领。
如清单 5 所示,假定我们要在客户端措施利用 X509TrustManager,那么就可以在 checkServerTrusted() 函数里做一些工作,检测随处事端证书异常的话,就可以做一些本身的处理惩罚。CheckClientTrusted() 则是用于处事端检测客户端的证书环境。
将清单 5 的代码替换到清单 4 的 TrustManager[] tms 的生成处,并对代码稍作调解即可。
清单 5. X509TrustManager 的利用
// 利用自界说的 MyTrustManager 发生信任库 TrustManager[] tms=new TrustManager[]{new MyTrustManager()}; …… …… class MyTrustManager implements X509TrustManager{ // 相关的 jks 文件及其暗码界说 private final static String TRUST_STORE="D:/test_client_trust.jks"; private final static String TRUST_STORE_PASSWORD="Testpassw0rd"; X509TrustManager xtm; public MyTrustManager() throws Exception { // 载入 jks 文件 KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(TRUST_STORE),TRUST_STORE_PASSWORD.toCharArray()); TrustManagerFactory tmf =TrustManagerFactory.getInstance("SunX509", "SunJSSE"); tmf.init(ks); TrustManager[] tms = tmf.getTrustManagers(); // 筛选出 X509 名目标信任证书 for (int i = 0; i < tms.length; i++) { if (tms[i] instanceof X509TrustManager) { xtm = (X509TrustManager) tms[i]; return; } } } // 处事端检讨客户端证书的接口 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException{ } // 客户端检讨处事端证书的接口 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException{ try{ xtm.checkServerTrusted(chain, authType); }catch(CertificateException excep){ System.out.println(excep.getMessage()); throw excep; } } // 获取可接管的刊行者 public X509Certificate[] getAcceptedIssuers() { //return xtm.getAcceptedIssuers(); return null; } }
留意:
1. 当处事端代码 setNeedClientAuth(False) 时,客户端的 MyTrustManager 实现了 X509TrustManager 后,假如 checkServerTrusted() 要领的实现为空,则无论处事端利用什么证书,客户端城市默认接管;假如要对处事端证书举办查抄,还需要像清单 5 中的代码片断那样,捕获异常并处理惩罚。
2.getAcceptedIssuers() 要领凡是不需要详细实现,可是当处事端要求检讨客户端身份,也即 setNeedClientAuth(True) 时,处事端需也需要详细实现 X509TrustManager,且 getAcceptedIssuers() 要领要如清单 5 中注释部门代码那样实现。
调试 SSL/TSL 措施
打开调试开关调查通信日志
图 2 描写了 SSL/TSL 通信的握手进程。在实际编写措施的时候,大概会在这些环节碰着问题,导致无法通信,排查起交往往令人无从下手。这个时候我们可以将 SSL/TSL 通信的握手日志开关打开,举办调查。
图 2.SSL 通信协议握手进程
(注:图片引自 《Java Secure Socket Extension (JSSE) Reference Guide》)
打开日志开关的要领同样是通过设定系统属性,可以从呼吁行添加虚拟机参数:
-Djavax.net.debug=ssl,handshake
虽然也可以利用 System.setProperty() 要领在代码中打开该开关。
打开日志开关后,可以搜索“ClientHello”、“ServerHello”等要害字;可能搜索“*** ”( 三个星号和一个空格 ) ——这是握手阶段每一个步调日志打印的开始符号。通过阅读日志来定位问题。更具体的开关信息,请参阅 JSSE 指导中的“Debugging Utilities”章节:
选择通信的 Cipher Suites
#p#分页标题#e#
有的时候为了做尝试,我们会选用特定的 Cipher Suites,我们可以利用 SSLServerSocket 或 SSLSocket 的 getEnabledCipherSuites() 调查到默认支持的所有加密套件的信息,然后利用 setEnabledCipherSuites() 举办配置。
清单 6 展示了从处事端过滤掉所有的匿名加密套件的代码。
清单 6. 过滤所有的匿名加密套件
String enabled[]=serverSocket.getEnabledCipherSuites(); Set<String> filter = new LinkedHashSet<String>(); for(int i = 0; i < enabled.length; i++) { if(enabled[i].indexOf("anon")<0){ filter.add(enabled[i]); } } serverSocket.setEnabledCipherSuites(filter.toArray(new String[filter.size()]));
竣事语
通过对本文的进修,我们可以快速相识如何利用 keytool 建造证书库和信任库,并利用 Java 编写安详套接字措施。文中贴出的呼吁行和代码片断都可以直接用于试验和实际应用中,可是要纯熟把握和领略,还发起凭据本文的步调,亲自动手试一试。