前面介绍了Socket的基本使用,这里开始介绍Socket的安全问题,作为一个Internet用户,你确实有一些保护手段可以保护自己的隐私不被泄露,为了使Internet连接从根本上上更加安全,可以对Socket进行加密,这可以保持事务的机密性、真实性和准确性。Java安全Socket扩展可以使用安全Socket层版本3和传输层安全协议以及相关算法来保护网络通信的安全。SS(Secure Sockets Layer,SSL)是一种安全协议,允许Web浏览器和其他TCP客户端基于各种级别的机密性和认证于HTTP和其他TCP服务对话。
经过开放通道(如公共Internet)的秘密通信绝对需要对数据加密。适合计算机实现的大多数加密机制是基于密钥思想的,密钥是一种更加一般化的口令,并不限于文本。明文消息根据一种数学算法与密钥的各个位组合,生成加密的密文。使用的密钥位数越多,通过强力猜测密钥的方法来解密就会越困难。在传统的密钥(或称为对称密钥)加密中,加密和解密数据都使用相同的密钥。发送方和接受方必须都知道这个密钥,所以发送消息之前发送方需要把这个密钥发送给接受方。但由于密钥是不能加密的,这导致第三方可以获取该密钥来偷窥发送方和接受方所有消息。在公开密钥(或非对称密钥)加密中,加密和解密数据使用不同的密钥。一个密钥称为公开密钥(public key),用于加密数据。这个密钥可以提供给任何人。另一个密钥称为私有密钥经济(private key),用于解密密钥。私有密钥必须保存好,只有通信中的一方拥有它。现在发送方给接受方发送了一条加密消息,接受方收到后向发送方询问公开密钥,这个公开密钥第三方也可以获取,但第三方并不能用这个公开密钥解密文件,因为解密需要接受方的私有密钥,所以在这种情况下信息是 安全的。非对称加密也可用于身份认证和消息完整性检查,这样接受方就能知道消息在传送过程中消息有没有被篡改。这样就保证了机密性、真实性和完整性。但非对称加密也不是绝对安全的,考虑下面场景:
第三方可以不攻击加密算法,而是攻击用于收发消息的协议,这种攻击不需要第三方破解密文,与密钥长度也完全无关。在接受方将其公开密钥发送给发送方是,第三方不只是可以读取这个公开密钥,还可以用他自己的公开密钥替换接受方的公开密钥。这样发送方就认为他正在使用接受方的密钥加密消息,其实使用的是第三方的公开密钥。然后继续发送给接受方。这样第三方就可以拦截到这个消息,使用自己的私有密钥解锁。这种攻击被称为
中间人攻击
。
上面可以看出加密/解密算法还是很麻烦的事,幸运的是我们不需要成为密码专家就可以在java网络程序中使用强加密。JSSE掩盖了如何协商算法、交换密钥、认证通信双方和加密数据的底层细节。JSSE允许你创建Socket和服务器Socket,可以透明地处理安全通信中 必要的协商和加密。你要做的就是熟悉流和Socket来发送数据(前面的内容)。Java安全Socket拓展分为四个包:
java.net.ssl
:定义Java安全网络API的抽象类
javax.net
:替代构造函数创建安全Socket的抽象Socket工厂
java.security.cert
:处理SSL所需公开密钥证书的类
com.sun.net.ssl
L:Sun公司的JSSE成参考实现中实现加密算法和协议的具体类。
如果不太关心底层的细节,使用加密SSL Socket与现有的安全服务器通信确实非常简单。并不是构造函数来构造一个java.net.Socket对象,而是从javax.net.ssl.SSLSocketFactory使用其createSocket()方法得到一个Socket对象。SSLSocketFactory使用遵循抽象工厂设计模式的抽象类。要通过调用静态SSLSocketFactory.getDefault()方法得到一个实例:
SocketFactory factory=SSLSocketFactory.getDefault();
Socket socket=factory.createSocket("login.ibiblio.org",7000);
一旦有了工厂的引用就可以使用下面5个重载方法createSocket()方法创建一个SSLSocket:
public abstract Socket createSocket(String host,int port) throws IOException,UnkonwHostException
public abstract Socket createSocket(InetAddress host,int port) throws IOException
//上面连个方法创建并返回一个连接到指定主机和端口的Socket
public abstract Socket createSocket(String host,int port,InetAddress interface,int localPort) throws IOException,UnkonwHostException
public abstract Socket createSocket(InetAddress host,int port,InetAddress interface,int localPort) throws IOException,UnkonwHostException
//上面两个方法创建并返回一个从指定本机网络接口和端口指定到指定主机和端口的Socket
public abstract Socket createSocket(Socket proxy ,String host,int port,boolean autoClose) throws IOException,UnkonwHostException
//上面的方法会使用到代理服务器
下面的代码会连接一个安全的HTTP服务器,发送简单的GET请求并显示响应
public class QuizCardBuilder {
public static void main(String[] args) {
int port=443; //默认的https端口
SSLSocketFactory factory=(SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket=null;
try{
String host="www.baidu.com";
socket=(SSLSocket) factory.createSocket(host ,port);
//启用所有密码组
String[] supported=socket.getSupportedCipherSuites();
socket.setEnabledCipherSuites(supported);
Writer out=new OutputStreamWriter(socket.getOutputStream(),"UTF-8");
//https在get行中需要完全URL
out.write("GET http://"+host+"/ HTTP/1.1\r\n");
out.write("Host: "+host+"\r\n");
out.write("\r\n");
out.flush();
//读取响应
BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//读取首部
String s;
while(!(s=in.readLine()).equals("")){
System.out.println(s);
}
System.out.println();
//读取长度
String contentLength=in.readLine();
int length=Integer.MAX_VALUE;
try{
//16进制字符串转换为int类型
length=Integer.parseInt(contentLength.trim(),16);
}catch (NumberFormatException e){
}
System.out.println(contentLength);
int c;
int i=0;
while((c=in.read())!=-1 && i++<length){
System.out.println(c);
}
System.out.println();
}catch (IOException ex){
System.err.println(ex);
}finally {
try{
if(socket!=null) socket.close();
}catch (IOException e){}
}
}
}
运行这个程序会发现比之前直接使用Socket要慢,这是因为生成和交换密钥时,都有相当可观的CPU和网络开销。
JSSE的不同实现支持认证和加密算法的不同组合。SSLSocketFactory
中的getSupportedCipherSuited()
方法可以指出给定Socket上可用的算法组合:
public abstract String[] getSupportedCipherSuites()
不过,并非所有能够理解的密码组都一定能在连接上使用。有些强度太弱,因而禁止使用。SSLSocketFactory
的getEnabledCipherSuites()
方法可以指出这个Socket允许使用哪些密码组:
public abstract String[] getEnabledCipherSuites()
实际使用的密码组要在连接时由客户端和服务器协商。可以通过setEnaledCipherSuites()
方法修改客户端试图使用的密码组
public abstract void setEnabledCipherSuites(String[] suites)
这个方法的参数应当是希望使用的密码组列表,列表中的每一个名必须是getSupportedCipherSuites()
列出的某个密码组。下面代码可以查看自己JDK支持的密码组:
public class QuizCardBuilder {
public static void main(String[] args) {
SSLServerSocketFactory sslServerSocketFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
String[] supportedCipherSuites = sslServerSocketFactory.getSupportedCipherSuites();
System.out.println("Supported Cipher Suites:");
for (String cipherSuite : supportedCipherSuites) {
System.out.println(cipherSuite);
}
}
}
输出结果如下:
TLS_AES_256_GCM_SHA384
TLS_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
TLS_DHE_DSS_WITH_AES_256_GCM_SHA384
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
TLS_DHE_DSS_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
TLS_DHE_DSS_WITH_AES_256_CBC_SHA256
TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
TLS_DHE_DSS_WITH_AES_128_CBC_SHA256
TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_DSS_WITH_AES_256_CBC_SHA
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_DSS_WITH_AES_128_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_CBC_SHA256
TLS_RSA_WITH_AES_128_CBC_SHA256
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_EMPTY_RENEGOTIATION_INFO_SCSV
每个名中的算法分为4个部分:协议、密钥交换算法和校验和。如TLS_AES_256_GCM_SHA384
表示在TLS通信中使用AES加密算法(密钥长度为256位)和GCM加密模式进行数据加密,同时使用SHA384算法生成消息摘要。这个密码套件提供了强大的加密和安全性,用于保护通信数据的机密性和完整性。一般来讲,以TLS_ECDHE开头的并以SHA256或SHA384结尾的密码组是当前使用最广泛的加密算法。
DES和AES区别:它们是块加密(即一次加密一定数量的二进制位),DES总是加密64位,编码器需要使用额外的位填充输入。AES可以加密128、192或256位的块,但如果不是块大小的整数倍,仍然需要填充输入。RC4是一种流加密,可以一次加密一个字节,更适合一次发送一个字节的协议。
String[] strongSuites={"TLS_ECDHE_ECDSA_WITH_AES_128_CBC=SHA256"}
socket.setEnabledCipherSuites(StrongSuites);
如果连接另一端不支持这个加密协议,就会抛出异常。
网络通信相对大多数计算机速度而言都很慢。认证的网络通信甚至更慢。安全连接所必需的密钥生成和建立过程会轻而易举地花费数秒钟时间。因此,你可能希望异步地处理连接。JSSE使用标准Java事件模型来通知程序,告诉它们客户端和服务器之间的握手何时完成。为了得到握手结束事件的通知,只需要实现HandshakeCompletedListener
接口
public void HandshakeCompleted(HandshakeCompletedEvent event)
//获取与当前SSLSocket关联的SSLSession对象。
public SSLSession getSession()
//获取当前SSL连接使用的密码套件。返回一个字符串,表示正在使用的密码套件的名称。密码套件包括加密算法、哈希算法和密钥交换算法等
public String getCipherSuite()
//获取对等方(服务器)的证书链。返回一个X509Certificate数组,表示对等方的证书链。如果对等方没有提供有效的证书链,或者未进行验证,将抛出SSLPeerUnverifiedException异常。
public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException
//获取底层的SSLSocket对象。返回与当前SSLSocket对象关联的底层SSLSocket实例。可以使用这个方法来进一步操作底层的SSLSocket,例如获取底层的InputStream和OutputStream。
public SSLSocket getSocket()
通过addHandshakeCompletedListener()
和removeHandshakeCompletedListener()
方法,特定的HandshakeCompletedListener
对象可以注册对某个SSLSocket的握手结束事件的关注:
public abstract void addHandshakeCompletedListener(HandshakeCompletedListener listener)
public abstract void removeHandshakeCompletedListener(HandshakeCompletedListener listener)throws IllegalArgumentException
下面是使用的一个案例
public static void main(String[] args) throws IOException {
try {
// 创建Socket并连接到服务器
SSLSocketFactory socketFactory=(SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket= (SSLSocket) socketFactory.createSocket("www.qq.com",443);
// 注册握手完成事件监听器
socket.addHandshakeCompletedListener(HandshakeCompletedEvent-> {
try {
LOGGER.info("Connected [" + HandshakeCompletedEvent.getSource() + ", " + socket.getSession().getPeerCertificateChain()[0].getSubjectDN() + "]");
} catch (SSLPeerUnverifiedException e) {
LOGGER.log(Level.WARNING,e.getMessage(), e);
}
});
//开始握手过程
socket.startHandshake();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
SSL常用于web服务器,因为web连接一般是暂时的,每个页面需要单独的Socket,如果每个页面搜要花一定时间来协商一个安全连接,会需要大量的握手时间 ,会产生很大的开销。SSL允许建立扩展到多个Socket的会话。相同会话中的不同Socket使用同一组相同的公开密钥和私有密钥。使用JSSE不需要任何额外的工作,如果在很短的时间内对一台主机的一个端口打开多个Socket(注意同一时间一个端口只能绑定一个Socket),JSSE会自动重用这个会话的密钥,在JSSE中,会话由SSLSession接口的实例表示,可以使用这个接口的方法来检查会话的创建时间和最后访问时间、将会话作废、得到会话的各种信息等:
在基于TCP的Socket通信中,通过建立客户端和服务器之间的连接,可以进行双向的数据传输。这个连接可以被看作是一个会话,但并非在Socket层面上定义的会话。相反,在基于传输层安全性(TLS)的Socket通信中,会话(session)是一个概念,用于表示建立在安全传输层上的连接。在TLS会话中,包括以下内容:
- 握手过程:在TLS会话开始时,客户端和服务器之间进行握手协商,包括协议版本、密码套件、加密算法等的协商过程。
- 安全参数:在握手完成后,会话包含了一组安全参数,如会话密钥、证书、协商的加密算法等。
- 会话重用:为了提高性能,TLS会话可以进行重用。客户端和服务器可以在之后的连接中重用之前建立的会话,避免重新执行完整的握手过程。
需要注意的是,TLS会话是在TLS/SSL协议层面定义的概念,并不是在标准的Socket层面上存在。因此,在使用标准的Socket通信时,并没有对会话的明确定义和管理。如果您需要在Socket通信中实现会话管理,您可以考虑使用其他协议或技术,如HTTP的会话管理(使用Cookie/Session)或WebSocket协议等。这些协议或技术在应用层面上提供了会话的概念和管理机制。
public byte[] getId()
public SSLSessionContext getSessionContext()
public long getCreationTime()
public long getLastAccessedTime()
public void invalidate()
public void putValue(String name,Object value)
public Object getValue(String name)
public void removeValue(string name)
public String[] getValueNames()
public String[] getValueNames()
public X509Certificate[] getPeerCertificateChain()throws SSLPeerUnverifiedException
public String getCipherSuite()
public String getPeerHost()
SSLSocket类的getSession()方法返回这个Socket所属的Session会话。不过会话和性能是一个折中。每一个事务都重新协商密钥会更加安全。为避免Socket创建会话,要向setEnableSessionCreation()
传入false。在很少见的情况下,你甚至希望重新认证一个连接(也就是丢弃前面协商好的所有密钥和证书,重新开启一个新的会话), startHandshake()
方法可以做到这一点。