本文是《大型分布式网站架构设计与实践》 3.5节HTTPS协议的学习笔记。
HTTPS和SSL
HTTPS的全称是Hypertext Transfer Protocol over Secure Socket Layer,即 SSL之上的HTTP。 SSL及其继任者TLS是应用层(HTTP)和传输层(TCP)之间的安全协议层。它可以实现通信双方的认证、通信内容的加密传输。下图是SSL协议的示意图:
上面的握手过程看起来非常复杂,其实这个过程中主要就做了两件事:
1. 证书的认证
2. 通信双方协议产生一个通信秘钥。
关于第二点需要说明的是: 通信双方真正通信时使用的是对称加密。 在通信开始之前,通信双方会协商出一个对称加密的秘钥,而在这个协商过程中会使用非对称加密来传输信息。这样就既能保证通信的效率又解决了对称加密秘钥分发存在风险的问题。
在握手完成之后,通信双方就可以加密传输了。
使用JSSE实现SSL/TSL
JSSE全称是 java security socket extension,我们下面将使用JSSE来实现SSL。
我们首先需要使用上一篇中的生成的证书导出为Java环境可用的keystore文件。
客户端证书导出:
服务器端证书导出:
将CA根证书导出为信任库:
使用SSLServerSocket 、SSLSocket和普通socket并没有太大的不同,只不过需要在使用之前加载证书和信任库而已,如下代码:
SSL服务端:
package server; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.TrustManagerFactory; public class Server { public static SSLServerSocket serverSocket; public static void init() throws Exception{ int port = 1234; //服务端keystore文件 String keystorePath = "/home/massclouds/keystores/server.keystore"; String keystorePassword = "1234"; //由根证书导出的信任库 String trustKeystorePath = "/home/massclouds/keystores/ca-trust.keystore"; String trustKeystorePassword = "123456"; //========= 加载服务端keystore文件和根证书信任库 begin =============== SSLContext sslContext = SSLContext.getInstance("SSL"); KeyManagerFactory kmf = KeyManagerFactory.getInstance("sunx509"); TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509"); KeyStore keyStore = KeyStore.getInstance("pkcs12"); KeyStore trustKeystore = KeyStore.getInstance("jks"); FileInputStream keyStoreFis = new FileInputStream(keystorePath); keyStore.load(keyStoreFis, keystorePassword.toCharArray()); FileInputStream trustKeyStoreFis = new FileInputStream(trustKeystorePath); trustKeystore.load(trustKeyStoreFis, trustKeystorePassword.toCharArray()); kmf.init(keyStore, keystorePassword.toCharArray()); tmf.init(trustKeystore); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); serverSocket = (SSLServerSocket)sslContext.getServerSocketFactory().createServerSocket(port); //========= 加载服务端keystore文件和根证书信任库 end =============== //是否需要客户端认证 serverSocket.setNeedClientAuth(true); } public static void process() throws Exception{ String bye = "bye bye !"; while(true){ Socket socket = serverSocket.accept(); byte[] inputBytes = new byte[1024]; InputStream input = socket.getInputStream(); input.read(inputBytes); System.out.println(new String(inputBytes)); OutputStream out = socket.getOutputStream(); out.write(bye.getBytes(), 0, bye.getBytes().length); out.flush(); } } public static void main(String[] args) throws Exception { init(); process(); } }
SSL客户端:
package client; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManagerFactory; public class Client { public static SSLSocket sslSocket ; public static void init() throws Exception{ String host = "localhost"; int port = 1234; //客户端 String keystorePath = "/home/massclouds/keystores/client.keystore"; String keystorePassword = "1234"; //CA String trustKeystorePath = "/home/massclouds/keystores/ca-trust.keystore"; String trustKeystorePassword = "123456"; SSLContext sslContext = SSLContext.getInstance("SSL"); KeyManagerFactory kmf = KeyManagerFactory.getInstance("sunx509"); TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509"); KeyStore keyStore = KeyStore.getInstance("pkcs12"); KeyStore trustKeyStore = KeyStore.getInstance("jks"); FileInputStream keyStoreFis = new FileInputStream(keystorePath); keyStore.load(keyStoreFis, keystorePassword.toCharArray()); FileInputStream trustKeyStoreFis = new FileInputStream(trustKeystorePath); trustKeyStore.load(trustKeyStoreFis, trustKeystorePassword.toCharArray()); kmf.init(keyStore, keystorePassword.toCharArray()); tmf.init(trustKeyStore); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); sslSocket = (SSLSocket)sslContext.getSocketFactory().createSocket(host, port); } public static void process() throws Exception{ String hello = "do what you love and keep doing !"; OutputStream output = sslSocket.getOutputStream(); output.write(hello.getBytes(), 0, hello.getBytes().length); output.flush(); byte[] inputBytes = new byte[1024]; InputStream input = sslSocket.getInputStream(); input.read(inputBytes); System.out.println(new String(inputBytes)); } public static void main(String[] args) throws Exception{ init(); process(); } }
在tomcat中部署HTTPS
实验环境是centos 7,tomcat所在目录是/usr/local/tomcat 。
我们将上面导出的keystore文件放在/root/keystore目录下
下面我们来配置 服务端单向认证
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS"
keystoreFile="/root/keystore/server.keystore"
keystorePass="1234"
keystoreType="pkcs12"
/>
配置完成后如果希望我们的服务器证书通过浏览器的验证还需要两步:
1. 讲在第一篇中生产的CA根证书导入到操作系统中。
2. 通过服务器证书中使用者的CN来配置客户端机器的域名映射,例如我的环境中需要配置
然后通过www.server.com来访问。这样就可以让服务器证书通过认证了。
在apache http server中配置https
实验环境 centos 7 Apache/2.4.6
默认情况下httpd是没有mod_ssl模块的,我们首先安装mod_ssl。
这个时候其实就已经支持https了,此时使用的证书和私钥是(配置文件是/etc/httpd/conf.d/ssl.conf):
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
此时服务器证书肯定是无法通过客户端认证的,启动服务访问:
我们从/etc/httpd/conf.d/ssl.conf中可以看到配置了一个443端口的virtual host,我们可以将这个虚拟主机中的 SSLCertificateFile SSLCertificateKeyFile 这两个指令配置为我们前面所讨论的服务器证书和私钥,这里有个问题是:我们上面在产生私钥时都使用 -aes256 参数加密了,所以每次在启动httpd时都会要求我们输入这个秘钥。 我们可以不用这个参数重新生产一遍服务器的私钥和证书(过程不再赘述)。
SSLCertificateKeyFile /etc/pki/tls/private/server-key.pem
剩下的工作和tomcat中就一样了,在客户端操作系统中导入根证书,配置服务器证书中对应的域名,然后就可以安全访问了。
将所有http请求重定向到https
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} !on
RewriteRule .* https://%{HTTP_HOST}/%{REQUEST_URI} [R=301,L,QSA]
</IfModule>