java 安全套接字扩展 是 在原有的socket之上 封装了一层SSL/TLS 这样的高级网络协议的实现,使得原有的socket 通讯变得安全。
1 SSL/TLS 协议介绍:
SSL 是洋文“Secure Sockets Layer”的缩写,中文叫做“安全套接层”。为啥要发明 SSL 这个协议捏?因为原先互联网上使用的 HTTP 协议是明文的,存在很多缺点——比如传输内容会被偷窥(嗅探)和篡改。发明 SSL 协议,就是为了解决这些问题。
到了1999年,SSL 因为应用广泛,已经成为互联网上的事实标准。IETF 就在那年把 SSL 标准化。标准化之后的名称改为 TLS(是“Transport Layer Security”的缩写),中文叫做“传输层安全协议”。
SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
2 ssl/tls 协议实现
SSL/TLS 协议本身只是概念,实现这个概念的供应商有好多 ,可以通过 Security.getProviders()
方法获取已注册提供者列表,然后 通过
- SSLContext sslContext = SSLContext.getInstance("SSL",”SunJSSE”); 或者
- SSLContext sslContext = SSLContext.getInstance("SSL");
来获取一个SSLContext ,sslcontext 对象 也是ssl 协议可以加在socket 之上,也可以加在http 之上。
加在socket 之上就变成了sslSocket ,加在http 之上就变成了 https
3 sslContext 的相关组件
Keystore、KeyManagerFactory、TrustManagerFactory、SSLContext 可以称之为“引擎类”(engine class),对它们指定特定的参数 ( 例如:协议、算法等 ),就可以产生符合我们要求的,用于编程的对象实例。
图 1. 相关类之间的关系
编程的步骤可以简单的小结为以下几步:
1. 使用 Keystore 类将证书库或信任库文件加载进来;
2. 使用 KeyManagerFactory 和加载了证书库的 Keystore 实例,产生 KeyManager 实例数组;
3. 使用 TrustManagerFactory 和加载了信任库的 Keystore 实例,产生 TrustManager 实例数组;
4. 使用 SSLContext 初始化 KeyManager 实例数组和 TrustManager 实例数组,从而设定好通信的环境。
5. 利用 SSLContext 产生的 SSLSocket 或 SSLServerSocket 进行通信
① 通信核心类——SSLSocket和SSLServerSocket。对于使用过socket进行通信开发的朋友比较好理解,它们对应的就是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工厂类生成。JSSE KeyManager负责选择向对等体显示哪些凭据。 许多算法是可能的,但是常见的策略是在由磁盘文件支持的KeyStore中维护RSA或DSA公钥/私钥对以及X509Certificate。 当从文件初始化并加载KeyStore对象时,文件的原始字节将使用KeyFactory转换为PublicKey和PrivateKey对象,并使用CertificateFactory转换证书链的字节。 当需要证书时,KeyManager简单地参考这个KeyStore对象,并确定出现哪些证书。
⑦ 信任管理器——TrustManager。此接口负责判断决定是否信任对方的安全证书,TrustManager对象由TrustManagerFactory工厂类生成。JSSE TrustManager负责验证从对等端收到的凭证。 验证凭证有多种方式:其中之一是创建CertPath对象,并让JDK的内置公钥基础结构(PKI)框架处理验证。 在内部,CertPath实现可能会创建一个Signature对象,并使用它来验证证书链中的每个签名。
⑧ 密钥证书存储设施——KeyStore。这个对象用于存放安全证书,安全证书一般以文件形式存放,KeyStore负责将证书加载到内存。
客户端模式 介绍:
关于身份认证方面有个名词叫客户端模式,一般情况客户端要对服务器端的身份进行验证,但是无需向服务器证实自己的身份,这样不用向对方证实自己身份的通信端我们就说它处于客户模式,否则成它处于服务器模式。SSLSocket的setUseClientMode(Boolean mode)方法可以设置客户端模式或服务器模式。
4 ssl协议 通讯步骤或者原理: 有了这个架构的基本理解,我们可以看一下SSL / TLS握手中的一些步骤:
1)客户端首先发送一个ClientHello消息给服务器。
2) 服务器尝试使用基于RSA的密码组,例如TLS_RSA_WITH_AES_128_CBC_SHA, 查询服务器的KeyManager,并返回相应的RSA条目, 服务器的凭证(即:证书/公钥)将在服务器的证书消息中发送。
3)客户端的TrustManager验证服务器的证书,如果接受,客户端使用SecureRandom对象生成一些随机字节。 然后使用已使用在服务器证书中找到的PublicKey初始化的加密非对称RSA密码对象对其进行加密。 此加密数据在客户端密钥交换消息中发送。
4)服务器将使用其相应的PrivateKey在解密模式下使用类似的密码恢复字节。 这些字节然后用于建立实际的加密密钥。
5)一旦建立了真实的加密密钥,秘密密钥就被用来初始化一个对称的密码对象,并且这个密码被用来保护所有传输中的数据。
6)为了帮助确定数据是否已被修改,创建MessageDigest并接收发往网络的数据的副本。 当数据包完成时,摘要(哈希)被附加到数据,并且整个数据包被密码加密。 如果使用诸如AES的分组密码,则必须填充数据以形成完整的块。 在另一端,这些步骤简单地颠倒过来。
根据ssl 协议通讯原理和ssl类库可以知道,
不论是服务端还是客户端 要想获取一个sslcontext 必须得需要 keymanager 和 trustManger ,
当创建服务器端上下文的时候 ,必须指定keymanager 这样才能向客户端发送证书( 对应步骤2),如果不校验客户端证书,可以不设置 trustManger,如果校验客户端证书,必须设置trustManager.
当创建服客户端上下文的时候 ,必须指定trustManger 这样才能校验服务器的证书( 对应步骤3),如果不校验客户端证书,可以不设置 trustManger,如果服务器设置了校验客户端证书,客户端必须指定keymanager.
那如何设置 trustManger 和 keymanager 呢?
1)通过jvm 系统属性指定truststore和keystore 的形式。
2) 通过java api 接口指定
5 通过系统属性指定证书库和信任库
这种编写方式比较简单直观,可以通过给 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 通信服务端程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
|
客户端程序
对应于清单 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"
-Djavax.net.ssl.keyStorePassword="Testpassw0rd"
清单 2. 简单的 SSL 通信客户端程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
SSLSocketFactory sf=(SSLSocketFactory)SSLSocketFactory.getDefault(); 获取keystore 和truststore 的逻辑是什么?
-
如果定义了系统属性“javax.net.ssl.trustStore”,则 TrustManager 将尝试使用由该系统属性指定的文件名来查找默认的 trustStore 文件。
-
如果未指定“javax.net.ssl.trustStore”系统属性并且文件“<java-home>/jre/lib/security/jssecacerts”存在,则使用该文件。
-
如果文件“<java-home>/jre/lib/security/cacerts”存在,则使用该文件。
以下使用java api 的方式设置keymanager 和 trustmanger
6 sslcontext 的使用 之socket 通讯 使用 sslcontext 示例
服务端代码
public class TomcatSSLServer {
privatestatic final String SSL_TYPE = "SSL";
privatestatic final String KS_TYPE = "JKS";
privatestatic final String X509 = "SunX509";
privatefinal static int PORT = 443;
privatestatic TomcatSSLServer sslServer;
privateSSLServerSocket svrSocket;
publicstatic TomcatSSLServer getInstance() throws Exception {
if(sslServer == null) {
sslServer= new TomcatSSLServer();
}
returnsslServer;
}
privateTomcatSSLServer() throws Exception{
SSLContextsslContext = createSSLContext();
SSLServerSocketFactoryserverFactory = sslContext.getServerSocketFactory();
svrSocket=(SSLServerSocket) serverFactory.createServerSocket(PORT);
svrSocket.setNeedClientAuth(true);
String[]supported = svrSocket.getEnabledCipherSuites();
svrSocket.setEnabledCipherSuites(supported);
}
privateSSLContext createSSLContext() throws Exception{
KeyManagerFactorykmf = KeyManagerFactory.getInstance(X509);
TrustManagerFactorytmf = TrustManagerFactory.getInstance(X509);
String serverKeyStoreFile ="c:\\tomcat.jks";
StringsvrPassphrase = "tomcat";
char[]svrPassword = svrPassphrase.toCharArray();
KeyStoreserverKeyStore = KeyStore.getInstance(KS_TYPE);
serverKeyStore.load(newFileInputStream(serverKeyStoreFile), svrPassword);
kmf.init(serverKeyStore,svrPassword);
StringclientKeyStoreFile = "c:\\client.jks";
StringcntPassphrase = "client";
char[]cntPassword = cntPassphrase.toCharArray();
KeyStoreclientKeyStore = KeyStore.getInstance(KS_TYPE);
clientKeyStore.load(newFileInputStream(clientKeyStoreFile),cntPassword);
tmf.init(clientKeyStore);
SSLContextsslContext =SSLContext.getInstance(SSL_TYPE);
sslContext.init(kmf.getKeyManagers(),tmf.getTrustManagers(), null);
returnsslContext;
}
publicvoid startService() {
SSLSocketcntSocket = null;
BufferedReaderioReader = null;
PrintWriterioWriter = null;
StringtmpMsg = null;
while(true ) {
try{
cntSocket=(SSLSocket) svrSocket.accept();
ioReader= new BufferedReader(new InputStreamReader(cntSocket.getInputStream()));
ioWriter= new PrintWriter(cntSocket.getOutputStream());
while( (tmpMsg = ioReader.readLine()) != null) {
System.out.println("客户端通过SSL协议发送信息:"+tmpMsg);
tmpMsg="欢迎通过SSL协议连接";
ioWriter.println(tmpMsg);
ioWriter.flush();
}
}catch(IOException e) {
e.printStackTrace();
}finally {
try{
if(cntSocket!= null) cntSocket.close();
}catch(Exception ex) {ex.printStackTrace();}
}
}
}
publicstatic void main(String[] args) throws Exception {
TomcatSSLServer.getInstance().startService();
}
}
基本顺序是先得到一个SSLContext实例,再对SSLContext实例进行初始化,密钥管理器及信任管理器作为参数传入,证书管理器及信任管理器按照指定的密钥存储器路径和密码进行加载。接着设置支持的加密套件,最后让SSLServerSocket开始监听客户端发送过来的消息。
客户端代码
public class TomcatSSLClient {
privatestatic final String SSL_TYPE = "SSL";
privatestatic final String X509 = "SunX509";
privatestatic final String KS_TYPE = "JKS";
privateSSLSocket sslSocket;
publicTomcatSSLClient(String targetHost,int port) throws Exception {
SSLContextsslContext = createSSLContext();
SSLSocketFactorysslcntFactory =(SSLSocketFactory) sslContext.getSocketFactory();
sslSocket= (SSLSocket) sslcntFactory.createSocket(targetHost, port);
String[]supported = sslSocket.getSupportedCipherSuites();
sslSocket.setEnabledCipherSuites(supported);
}
privateSSLContext createSSLContext() throws Exception{
KeyManagerFactorykmf = KeyManagerFactory.getInstance(X509);
TrustManagerFactorytmf = TrustManagerFactory.getInstance(X509);
StringclientKeyStoreFile = "c:\\client.jks";
StringcntPassphrase = "client";
char[]cntPassword = cntPassphrase.toCharArray();
KeyStoreclientKeyStore = KeyStore.getInstance(KS_TYPE);
clientKeyStore.load(newFileInputStream(clientKeyStoreFile),cntPassword);
StringserverKeyStoreFile = "c:\\tomcat.jks";
StringsvrPassphrase = "tomcat";
char[]svrPassword = svrPassphrase.toCharArray();
KeyStoreserverKeyStore = KeyStore.getInstance(KS_TYPE);
serverKeyStore.load(newFileInputStream(serverKeyStoreFile), svrPassword);
kmf.init(clientKeyStore,cntPassword);
tmf.init(serverKeyStore);
SSLContextsslContext =SSLContext.getInstance(SSL_TYPE);
sslContext.init(kmf.getKeyManagers(),tmf.getTrustManagers(), null);
returnsslContext;
}
publicString sayToSvr(String sayMsg) throws IOException{
BufferedReaderioReader = new BufferedReader(new InputStreamReader(
sslSocket.getInputStream()));
PrintWriterioWriter = new PrintWriter(sslSocket.getOutputStream());
ioWriter.println(sayMsg);
ioWriter.flush();
returnioReader.readLine();
}
publicstatic void main(String[] args) throws Exception {
TomcatSSLClientsslSocket = new TomcatSSLClient("127.0.0.1",443);
BufferedReaderioReader = new BufferedReader(new InputStreamReader(System.in));
StringsayMsg = "";
StringsvrRespMsg= "";
while((sayMsg = ioReader.readLine())!= null ) {
svrRespMsg= sslSocket.sayToSvr(sayMsg);
if(svrRespMsg!= null && !svrRespMsg.trim().equals("")) {
System.out.println("服务器通过SSL协议响应:"+svrRespMsg);
}
}
}
}
注意服务器端有行代码svrSocket.setNeedClientAuth(true);它是非常重要的一个设置方法,用于设置是否验证客户端的身份。
假如我们把它注释掉或设置为false,则认为服务器不校验客户端证书,服务器不需要设置自己的信任管理器,即服务器不需要通过client.jks对客户端的身份进行验证,此时客户端将不再需要设置自己的密钥管理器( keymanager),客户端把密钥管理器(keymanager)直接设置为null也可以跟服务器端进行通信。
X509 证书信任管理器
使用 TrustManager 接口,我们已经可以在程序中自定义信任库了,但如果对方的证书不在信任库中,则通信会直接宣告失败。
X509TrustManager 接口扩展了 TrustManager 接口,我们可以使用 X509TrustManager 接口,实现自己的方法,自定义信任库的一些行为 ( 例如:检验对方证书,针对异常做一些处理 )
假定我们要在客户端程序使用 X509TrustManager,那么就可以在 checkServerTrusted() 函数里做一些事情,检测到服务端证书异常的话,就可以做一些自己的处理。CheckClientTrusted() 则是用于服务端检测客户端的证书情况
X509TrustManager 的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
getAcceptedIssuers() 方法通常不需要具体实现,但是当服务端要求检验客户端身份,也即 setNeedClientAuth(True) 时,服务端需也需要具体实现 X509TrustManager,且 getAcceptedIssuers() 方法要如清单 5 中注释部分代码那样实现
6 sslcontext 的使用 之https 通讯 使用 sslcontext 示例
import javax.net.ssl.*;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class HttpsUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpsUtil.class);
private static String charset = "utf-8";
private static class TrustAnyTrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[] {};
}
}
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
/**
* post方式请求服务器(https协议)
*
* @param url
* 请求地址
* @param content
* 参数
* 编码
* @return
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
* @throws IOException
*/
public static String post(String url, String content, Map<String,String> headers, int connTimeOut, int soTimeOut ) {
SSLContext sc =null; //构造一个sslcontext上下文,这个上下文里记录了会信任哪些服务器的证书以及自己上传的证书
try{
sc=SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() },
new java.security.SecureRandom());
}catch (Exception e){
LOGGER.error("getSSLContextError: ", e);
return URLConnUtil.EXCEPTION_MAP.get("GET_SSL_EXCEPTION");
}
URL console =null;
try{
console=new URL(url);
}catch (Exception e){
LOGGER.error("urlException: ", e);
return URLConnUtil.EXCEPTION_MAP.get("URI_SYNTAX_EXCEPTION");
}
HttpsURLConnection conn =null;
try{
conn=(HttpsURLConnection) console.openConnection();
}catch (Exception e){
LOGGER.error("IO_EXCEPTION: ", e);
return URLConnUtil.EXCEPTION_MAP.get("IO_EXCEPTION");
}
//conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
if(headers!=null){
Iterator<String> keys= headers.keySet().iterator();
while(keys.hasNext()){
String key=keys.next();
conn.setRequestProperty(key,headers.get(key));
}
}
conn.setSSLSocketFactory(sc.getSocketFactory()); //把初始化的上下文作为https 连接的一个属性设置进去
conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); // 设置连接是否校验 hostname
conn.setDoOutput(true);
conn.setConnectTimeout(connTimeOut);
conn.setReadTimeout(soTimeOut);
try{
conn.connect();
}catch (Exception e){
LOGGER.error("IO_EXCEPTION: ", e);
return URLConnUtil.EXCEPTION_MAP.get("IO_EXCEPTION");
}
try {
DataOutputStream out=new DataOutputStream(conn.getOutputStream());
out.write(content.getBytes(charset));
// 刷新、关闭
out.flush();
out.close();
InputStream is = conn.getInputStream();
if (is != null) {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
is.close();
return new String(outStream.toByteArray());
}
return null;
}catch (ConnectTimeoutException e) {
LOGGER.error("IO_EXCEPTION: ", e);
return URLConnUtil.EXCEPTION_MAP.get("ConnectTimeoutException");
} catch (UnsupportedEncodingException e) {
LOGGER.error("UnsupportedEncodingException: ", e);
return URLConnUtil.EXCEPTION_MAP.get("UnsupportedEncodingException");
} catch (ClientProtocolException e) {
LOGGER.error("ClientProtocolException: ", e);
return URLConnUtil.EXCEPTION_MAP.get("ClientProtocolException");
} catch (ParseException e) {
LOGGER.error("ParseException: ", e);
return URLConnUtil.EXCEPTION_MAP.get("ParseException");
} catch (IOException e) {
LOGGER.error("IO_EXCEPTION: ", e);
return URLConnUtil.EXCEPTION_MAP.get("IOException");
} catch (Exception e) {
LOGGER.error("EXCEPTION: ", e);
return URLConnUtil.EXCEPTION_MAP.get("EXCEPTION");
}
}
}