13.5 安全套接字连接

在各种数据传输方式中,网络传输目前使用较广,存在的安全隐患也较多。数据在网络传输过程中可能被第三方窃取或篡改。为了保护数据的安全,在涉及网络传输的程序中,可以使用安全套接字层(Secure Sockets Layer, SSL)协议。SSL协议在TCP/IP协议栈中位于传输层协议(TCP)和应用层协议(包括HTTP、Telnet和FTP等)之间。SSL比较常见的使用方式是与HTTP一起使用,用来构建安全的Web应用。SSL协议最早由Netscape公司开发,现在是由IETF组织维护的国际标准。标准化之后的SSL协议改名为传输层安全协议(Transport Layer Security, TLS)。Java SE 7中默认的实现支持TLS 1.1和TLS 1.2协议。

13.5.1 SSL协议

SSL协议的目的在于解决网络传输中存在的3个安全问题。第一个问题是身份认证,即确保当前正在通信的对等体的身份是合法的。这点在Web应用中尤其重要。如果不解决身份认证的问题,那么用户会将攻击者创建的钓鱼网站误认为是正确的网站。攻击者通过这种方式可以盗取用户的信息。SSL协议允许通信的双方在建立数据连接之前,先进行身份验证。在身份验证中使用经过数字签名的证书。第二个问题是数据被窃取。SSL协议在传输数据的过程中会对数据进行加密,这样可以保证即便在数据泄露的情况下,其中所包含的信息不会被窃取。第三个问题是数据可能被篡改。SSL协议对传输的数据添加了消息验证码,接收者可以对数据的完整性进行校验。

在安全套接字连接建立之前,客户端和服务器端之间需要通过握手机制就连接的细节达成一致。当握手结束之后,双方可以按照约定的方式自由发送数据。SSL协议的握手过程比较复杂,大致分成如下几步。

1)客户端发出连接请求。请求中包含客户端所能支持的SSL协议的最高版本,以及所能使用的加密算法的信息。

2)服务器端接收到连接请求之后,根据客户端给出的信息,选择双方都能支持的最高版本的SSL协议,并确定双方都能使用的加密算法。服务器端把选择的结果发送给客户端。如果客户端要求认证服务器端的身份,那么服务器端把它所持有的数字证书发送给客户端。如果服务器端也需要认证客户端的身份,那么服务器端向客户端发出认证请求。如果服务器端发送的数字证书中包含的信息不足以在双方之间进行密钥交换,那么服务器端会发送额外的密钥信息。

3)接着由客户端来处理服务器端返回的响应内容。如果客户端收到了服务器端进行身份验证的请求,那么客户端使用自己的私钥对所持有的数字证书进行加密之后发送给服务器端。客户端生成在数据传输时进行加密操作所使用的密钥,并使用服务器端给出的公钥进行加密之后发送给服务器端。所有这些操作完成之后,客户端发出通知,切换到加密的数据传输方式。

4)最后由服务器端来完成整个握手过程。如果服务器端在第2)步中选择验证客户端身份,会先验证客户端发送过来的信息是否正确。接着同样切换到加密的数据传输方式。握手过程结束后双方进行数据传输,直到连接关闭。

上述握手过程中的一个重要组成部分是数字证书。数字证书中包含证书持有者的身份信息和公钥。在非对称加密算法中,公钥是公开的。使用公钥可以解决如何安全共享密钥的问题。但是还有一个问题是如何确保接收者所得到的公钥来自所声明的真实发送者,而不是伪造的。攻击者可以随意创建一个新的公钥私钥对,并声称该公钥属于另外一个公司或组织。数字证书的目的就是解决这个问题。证书由用户所信任的机构(Certification Authority)签发,并通过该机构的私钥来加密。数字证书持有者的真实性由信任机构来保证。在有些情况下,某个证书签发机构的真实性需要由另外一个机构的证书来证明。通过这种证明关系形成一个证书的链条,而链条的根是公认的值得信任的机构。只有当证书链条上的所有证书都被信任时,证书中所给出的公钥才能得到信任。支持SSL协议的应用,如浏览器,通常会把一些重要的信任机构的公钥保存起来,在验证时使用。对于一个证书,先使用这些保存的公钥来检验证书本身的合法性。证书检验合法之后,证明其中所包含的身份信息和公钥是真实的,可以使用这个证书中包含的公钥进行身份认证。

在SSL握手过程中的一个重要步骤是双方对数据传输时使用的密钥达成一致。数据传输过程中使用的是对称加密算法。服务器端和客户端持有相同的密钥,客户端把生成的密钥经过服务器端的公钥加密之后,发送给服务器端,服务器端用自己的私钥解密,就得到了双方共同使用的密钥。

对于开发人员来说,安全套接字的使用与普通的套接字连接并没有太大的区别。建立连接和数据传输时使用的不是java.net.Socket类和java.net.ServerSocket类,而是它们的子类javax.net.ssl.SSLSocket类和javax.net.ssl.SSLServerSocket类。相应的工厂类javax.net.ssl.SSLSocketFactory和类javax.net.ssl.SSLServerSocketFactory分别用来创建SSLSocket类和SSLServerSocket类的对象。在使用SSLSocket类和SSLServerSocket类的对象建立连接的过程中,相关的SSL握手机制是自动完成的。在使用过程中,先要创建javax.net.ssl.SSLContext类的对象,通过SSLContext类的对象的getSocketFactory和getServerSocketFactory方法可以得到用来创建套接字对象的SSLSocketFactory类和SSLServerSocketFactory类的对象。

SSLContext类也采用了标准的服务提供者机制,使用getInstance方法并指定SSL协议的版本可以创建出所需的对象。创建了SSLContext类的对象之后,一般需要调用init方法进行初始化。在初始化时需要提供3个参数:第一个参数是javax.net.ssl.KeyManager接口的实现对象,用于对SSL握手中所需的密钥进行管理,提供当前对等体所持有的公钥;第二个参数是javax.net.ssl.TrustManager接口的实现对象,用于根据当前对等体的证书判断是否信任与之通信的另一个对等体;第三个参数是java.security.SecureRandom类的对象,用于生成加密时所需的安全的随机数。