作者:kingszelda

cnblogs.com/kingszelda/p/9029735.html

一、背景

HTTP是一个传输内容有可读性的公开协议,客户端与服务器端的数据完全通过明文传输。在这个背景之下,整个依赖于Http协议的互联网数据都是透明的,这带来了很大的数据安全隐患。想要解决这个问题有两个思路:

  1. C/S端各自负责,即客户端与服务端使用协商好的加密内容在Http上通信

  2. C/S端不负责加解密,加解密交给通信协议本身解决

第一种在现实中的应用范围其实比想象中的要广泛一些。双方线下交换密钥,客户端在发送的数据采用的已经是密文了,这个密文通过透明的Http协议在互联网上传输。

服务端在接收到请求后,按照约定的方式解密获得明文。这种内容就算被劫持了也不要紧,因为第三方不知道他们的加解密方法。然而这种做法太特殊了,客户端与服务端都需要关心这个加解密特殊逻辑。

第二种C/S端可以不关心上面的特殊逻辑,他们认为发送与接收的都是明文,因为加解密这一部分已经被协议本身处理掉了。

从结果上看这两种方案似乎没有什么区别,但是从软件工程师的角度看区别非常巨大。因为第一种需要业务系统自己开发响应的加解密功能,并且线下要交互密钥,第二种没有开发量。

HTTPS是当前最流行的HTTP的安全形式,由NetScape公司首创。在HTTPS中,URL都是以https://开头,而不是http://。使用了HTTPS时,所有的HTTP的请求与响应在发送到网络上之前都进行了加密,这是通过在SSL层实现的。

二、加密方法

通过SSL层对明文数据进行加密,然后放到互联网上传输,这解决了HTTP协议原本的数据安全性问题。一般来说,对数据加密的方法分为对称加密与非对称加密。

2.1 对称加密

对称加密是指加密与解密使用同样的密钥,常见的算法有DES与AES等,算法时间与密钥长度相关。

对称密钥最大的缺点是需要维护大量的对称密钥,并且需要线下交换。加入一个网络中有n个实体,则需要n(n-1)个密钥。

2.2 非对称加密

非对称加密是指基于公私钥(public/private key)的加密方法,常见算法有RSA,一般而言加密速度慢于对称加密。

对称加密比非对称加密多了一个步骤,即要获得服务端公钥,而不是各自维护的密钥。

整个加密算法建立在一定的数论基础上运算,达到的效果是,加密结果不可逆。即只有通过私钥(private key)才能解密得到经由公钥(public key)加密的密文。

在这种算法下,整个网络中的密钥数量大大降低,每个人只需要维护一对公司钥即可。即n个实体的网络中,密钥个数是2n。

其缺点是运行速度慢。

2.3 混合加密

周星驰电影《食神》中有一个场景,黑社会火并,争论撒尿虾与牛丸的底盘划分问题。食神说:“真是麻烦,掺在一起做成撒尿牛丸那,笨蛋!”

对称加密的优点是速度快,缺点是需要交换密钥。非对称加密的优点是不需要交互密钥,缺点是速度慢。干脆掺在一起用好了。

混合加密正是HTTPS协议使用的加密方式。先通过非对称加密交换对称密钥,后通过对称密钥进行数据传输。

由于数据传输的量远远大于建立连接初期交换密钥时使用非对称加密的数据量,所以非对称加密带来的性能影响基本可以忽略,同时又提高了效率。

三、HTTPS握手

可以看到,在原HTTP协议的基础上,HTTPS加入了安全层处理:

  1. 客户端与服务端交换证书并验证身份,现实中服务端很少验证客户端的证书

  2. 协商加密协议的版本与算法,这里可能出现版本不匹配导致失败

  3. 协商对称密钥,这个过程使用非对称加密进行

  4. 将HTTP发送的明文使用3中的密钥,2中的加密算法加密得到密文

  5. TCP层正常传输,对HTTPS无感知

四、HttpClient对HTTPS协议的支持

4.1 获得SSL连接工厂以及域名校验器

作为一名软件工程师,我们关心的是“HTTPS协议”在代码上是怎么实现的呢?探索HttpClient源码的奥秘,一切都要从HttpClientBuilder开始。

public CloseableHttpClient build() {//省略部分代码HttpClientConnectionManager connManagerCopy = this.connManager;//如果指定了连接池管理器则使用指定的,否则新建一个默认的if (connManagerCopy == null) {LayeredConnectionSocketFactory sslSocketFactoryCopy = this.sslSocketFactory;if (sslSocketFactoryCopy == null) {//如果开启了使用环境变量,https版本与密码控件从环境变量中读取final String[] supportedProtocols = systemProperties ? split(System.getProperty("https.protocols")) : null;final String[] supportedCipherSuites = systemProperties ? split(System.getProperty("https.cipherSuites")) : null;//如果没有指定,使用默认的域名验证器,会根据ssl会话中服务端返回的证书来验证与域名是否匹配HostnameVerifier hostnameVerifierCopy = this.hostnameVerifier;if (hostnameVerifierCopy == null) {hostnameVerifierCopy = new DefaultHostnameVerifier(publicSuffixMatcherCopy);}//如果制定了SslContext则生成定制的SSL连接工厂,否则使用默认的连接工厂if (sslContext != null) {sslSocketFactoryCopy = new SSLConnectionSocketFactory(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifierCopy);} else {if (systemProperties) {sslSocketFactoryCopy = new SSLConnectionSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault(),supportedProtocols, supportedCipherSuites, hostnameVerifierCopy);} else {sslSocketFactoryCopy = new SSLConnectionSocketFactory(SSLContexts.createDefault(),hostnameVerifierCopy);}}}//将Ssl连接工厂注册到连接池管理器中,当需要产生Https连接的时候,会根据上面的SSL连接工厂生产SSL连接@SuppressWarnings("resource")final PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslSocketFactoryCopy).build(),null,null,dnsResolver,connTimeToLive,connTimeToLiveTimeUnit != null ? connTimeToLiveTimeUnit : TimeUnit.MILLISECONDS);//省略部分代码}
}

上面的代码将一个Ssl连接工厂SSLConnectionSocketFactory创建,并注册到了连接池管理器中,供之后生产Ssl连接使用。连接池的问题参考:http://www.cnblogs.com/kingszelda/p/8988505.html

这里在配置SSLConnectionSocketFactory时用到了几个关键的组件,域名验证器HostnameVerifier以及上下文SSLContext。

其中HostnameVerifier用来验证服务端证书与域名是否匹配,有多种实现,DefaultHostnameVerifier采用的是默认的校验规则,替代了之前版本中的BrowserCompatHostnameVerifier与StrictHostnameVerifier。NoopHostnameVerifier替代了AllowAllHostnameVerifier,采用的是不验证域名的策略。

注意,这里有一些区别,BrowserCompatHostnameVerifier可以匹配多级子域名,"*.foo.com"可以匹配"a.b.foo.com"。StrictHostnameVerifier不能匹配多级子域名,只能到"a.foo.com"。

而4.4之后的HttpClient使用了新的DefaultHostnameVerifier替换了上面的两种策略,只保留了一种严格策略及StrictHostnameVerifier。因为严格策略是IE6与JDK本身的策略,非严格策略是curl与firefox的策略。即默认的HttpClient实现是不支持多级子域名匹配策略的。

SSLContext存放的是和密钥有关的关键信息,这部分与业务直接相关,非常重要,这个放在后面单独分析。

4.2 如何获得SSL连接

如何从连接池中获得一个连接,这个过程之前的文章中有分析过,这里不做分析,参考连接:

http://www.cnblogs.com/kingszelda/p/8988505.html。

在从连接池中获得一个连接后,如果这个连接不处于establish状态,就需要先建立连接。

DefaultHttpClientConnectionOperator部分的代码为:

public void connect(final ManagedHttpClientConnection conn,final HttpHost host,final InetSocketAddress localAddress,final int connectTimeout,final SocketConfig socketConfig,final HttpContext context) throws IOException {//之前在HttpClientBuilder中register了http与https不同的连接池实现,这里lookup获得Https的实现,即SSLConnectionSocketFactory    final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(context);final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());if (sf == null) {throw new UnsupportedSchemeException(host.getSchemeName() +" protocol is not supported");}//如果是ip形式的地址可以直接使用,否则使用dns解析器解析得到域名对应的ipfinal InetAddress[] addresses = host.getAddress() != null ?new InetAddress[] { host.getAddress() } : this.dnsResolver.resolve(host.getHostName());final int port = this.schemePortResolver.resolve(host);//一个域名可能对应多个Ip,按照顺序尝试连接for (int i = 0; i < addresses.length; i++) {final InetAddress address = addresses[i];final boolean last = i == addresses.length - 1;//这里只是生成一个socket,还并没有连接Socket sock = sf.createSocket(context);//设置一些tcp层的参数sock.setSoTimeout(socketConfig.getSoTimeout());sock.setReuseAddress(socketConfig.isSoReuseAddress());sock.setTcpNoDelay(socketConfig.isTcpNoDelay());sock.setKeepAlive(socketConfig.isSoKeepAlive());if (socketConfig.getRcvBufSize() > 0) {sock.setReceiveBufferSize(socketConfig.getRcvBufSize());}if (socketConfig.getSndBufSize() > 0) {sock.setSendBufferSize(socketConfig.getSndBufSize());}final int linger = socketConfig.getSoLinger();if (linger >= 0) {sock.setSoLinger(true, linger);}conn.bind(sock);final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);if (this.log.isDebugEnabled()) {this.log.debug("Connecting to " + remoteAddress);}try {//通过SSLConnectionSocketFactory建立连接并绑定到conn上sock = sf.connectSocket(connectTimeout, sock, host, remoteAddress, localAddress, context);conn.bind(sock);if (this.log.isDebugEnabled()) {this.log.debug("Connection established " + conn);}return;} //省略一些代码}}

在上面的代码中,我们看到了是建立SSL连接之前的准备工作,这是通用流程,普通HTTP连接也一样。SSL连接的特殊流程体现在哪里呢?

SSLConnectionSocketFactory部分源码如下:

@Overridepublic Socket connectSocket(final int connectTimeout,final Socket socket,final HttpHost host,final InetSocketAddress remoteAddress,final InetSocketAddress localAddress,final HttpContext context) throws IOException {Args.notNull(host, "HTTP host");Args.notNull(remoteAddress, "Remote address");final Socket sock = socket != null ? socket : createSocket(context);if (localAddress != null) {sock.bind(localAddress);}try {if (connectTimeout > 0 && sock.getSoTimeout() == 0) {sock.setSoTimeout(connectTimeout);}if (this.log.isDebugEnabled()) {this.log.debug("Connecting socket to " + remoteAddress + " with timeout " + connectTimeout);}//建立连接sock.connect(remoteAddress, connectTimeout);} catch (final IOException ex) {try {sock.close();} catch (final IOException ignore) {}throw ex;}// 如果当前是SslSocket则进行SSL握手与域名校验if (sock instanceof SSLSocket) {final SSLSocket sslsock = (SSLSocket) sock;this.log.debug("Starting handshake");sslsock.startHandshake();verifyHostname(sslsock, host.getHostName());return sock;} else {//如果不是SslSocket则将其包装为SslSocketreturn createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context);}}@Overridepublic Socket createLayeredSocket(final Socket socket,final String target,final int port,final HttpContext context) throws IOException {//将普通socket包装为SslSocket,socketfactory是根据HttpClientBuilder中的SSLContext生成的,其中包含密钥信息final SSLSocket sslsock = (SSLSocket) this.socketfactory.createSocket(socket,target,port,true);//如果制定了SSL层协议版本与加密算法,则使用指定的,否则使用默认的if (supportedProtocols != null) {sslsock.setEnabledProtocols(supportedProtocols);} else {// If supported protocols are not explicitly set, remove all SSL protocol versionsfinal String[] allProtocols = sslsock.getEnabledProtocols();final List<String> enabledProtocols = new ArrayList<String>(allProtocols.length);for (final String protocol: allProtocols) {if (!protocol.startsWith("SSL")) {enabledProtocols.add(protocol);}}if (!enabledProtocols.isEmpty()) {sslsock.setEnabledProtocols(enabledProtocols.toArray(new String[enabledProtocols.size()]));}}if (supportedCipherSuites != null) {sslsock.setEnabledCipherSuites(supportedCipherSuites);}if (this.log.isDebugEnabled()) {this.log.debug("Enabled protocols: " + Arrays.asList(sslsock.getEnabledProtocols()));this.log.debug("Enabled cipher suites:" + Arrays.asList(sslsock.getEnabledCipherSuites()));}prepareSocket(sslsock);this.log.debug("Starting handshake");//Ssl连接握手sslsock.startHandshake();//握手成功后校验返回的证书与域名是否一致verifyHostname(sslsock, target);return sslsock;}

可以看到,对于一个SSL通信而言。首先是建立普通socket连接,然后进行ssl握手,之后验证证书与域名一致性。之后的操作就是通过SSLSocketImpl进行通信,协议细节在SSLSocketImpl类中体现,但这部分代码jdk并没有开源,感兴趣的可以下载相应的openJdk源码继续分析。

五、本文总结

  1. https协议是http的安全版本,做到了传输层数据的安全,但对服务器cpu有额外消耗

  2. https协议在协商密钥的时候使用非对称加密,密钥协商结束后使用对称加密

  3. 有些场景下,即使通过了https进行了加解密,业务系统也会对报文进行二次加密与签名

  4. HttpClient在build的时候,连接池管理器注册了两个SslSocketFactory,用来匹配http或者https字符串

  5. https对应的socket建立原则是先建立,后验证域名与证书一致性

  6. ssl层加解密由jdk自身完成,不需要httpClient进行额外操作

END

推荐好文

强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

为什么MySQL不推荐使用uuid或者雪花id作为主键?

为什么建议大家使用 Linux 开发?爽(外加七个感叹号)

IntelliJ IDEA 15款 神级超级牛逼插件推荐(自用,真的超级牛逼)

炫酷,SpringBoot+Echarts实现用户访问地图可视化(附源码)

记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统(附源码)

用了这么久,你真的真的明白 HttpClient 的实现原理了吗?相关推荐

  1. 你真的弄明白了吗?Java并发之AQS详解

    你真的弄明白了吗?Java并发之AQS详解 带着问题阅读 1.什么是AQS,它有什么作用,核心思想是什么 2.AQS中的独占锁和共享锁原理是什么,AQS提供的锁机制是公平锁还是非公平锁 3.AQS在J ...

  2. c语言sizeof char,sizeof 你真的弄明白了吗?来看看这个例子

    原标题:sizeof 你真的弄明白了吗?来看看这个例子 sizeof基础 在C语言中,sizeof是一个操作符(operator),而不是函数!其用于判断数据类型或者表达式长度(所占的内存字节数).其 ...

  3. 上线红包功能,真的真的没有你想的这么简单~

    ---- / BEGIN / ---- 年前玲子负责了自己产品的红包版本功能的大迭代,感触和收获颇深,觉得有必要做一次产品复盘的自我思考. 随着移动支付的发展,微信红包彻底改变了我们的红包文化,互联网 ...

  4. 仅此一文让你明白ASP.NET MVC原理

    ASP.NET MVC由以下两个核心组成部分构成: 一个名为UrlRoutingModule的自定义HttpModule,用来解析Controller与Action名称: 一个名为MvcHandler ...

  5. java中new与newitance_你真的弄明白 new 了吗

    好久没有写点东西了,总觉得自己应该写点牛逼的,却又不知道如何下笔.既然如此,还是回归最基本的吧,今天就来说一说这个new.关于javascript的new关键字的内容上网搜一搜还真不少,大家都说new ...

  6. 帷幕的帷是什么意思_《老酒馆》热播背后:你真的看明白大戏背后的意思了吗?...

    最近热播剧<老酒馆>非常火,里面陈宝国.秦海璐.冯雷.刘桦.程煜.冯恩鹤等人的表演固然精彩,剧情也很有意思,但这场大戏背后的意思却没有多少人看明白. 实际上,整部戏的戏眼就在前两集. 还记 ...

  7. 电解电容使用久了电容量真的会下降吗?

    这台SONY TRV705E Hi8摄录一体机说来已经有25年了,其中至少有20年处于收藏状态.近日想把录像带里的内容导出来,才发现其回放的图像质量到完全不能接受的程度.虽然是古董了,还是希望能把它修 ...

  8. CRC的计算过程你真的搞明白了吗??

    在网上搜索CRC的相关资料,一下搜出一大堆,但是基本上都是讲的比较笼统,几乎都是千篇一律,但是按着网上搜的例程算了一遍自己要计算的数据,但是结果与用CRC软件算出来的结果不一致,是不是自己算错了呢?? ...

  9. sizeof你真的弄明白了吗?来看看这个例子

    本文 转自「嵌入式大杂烩」,喜欢的同学可以关注~ sizeof基础 在C语言中,sizeof是一个操作符(operator),而不是函数!其用于判断数据类型或者表达式长度(所占的内存字节数).其有两种 ...

最新文章

  1. Docker对AUFS的使用
  2. 线性代数同济第六版_线性代数考试内容与课后习题
  3. 一个关于js所有函数都报错的问题
  4. Linux 防火墙工具--iptables
  5. BugKuCTF 杂项 猜
  6. javascript:this 关键字
  7. 简明Vim练级攻略(初学者)
  8. java默认代码地址_Java 8默认方法可能会破坏您的(用户)代码
  9. [js] Number()的存储空间是多大?假如接口返回一个超过最大字节的数字怎么办?
  10. r语言中的或怎么表示什么不同_R经典入门 之 R语言的基本原理与概念 -- 200430
  11. linux 释放进程res_linux下查询进程占用的内存方法总结
  12. 利用百度图像识别鉴定植物
  13. Python制作一个简单的图片文字提取+PDF转WORD的软件
  14. 直播技术——流媒体协议
  15. 虚拟化平台就在你身边-ARM架构虚拟化扩展, 以及SMMU对虚拟化等解决方案的重要性(白皮书 2010)
  16. 操作系统-处理机调度(调度层次、基本准则、先来先服务、最短作业优先、高响应比、时间片轮转、优先级调度、多级反馈队列)
  17. python场景文字识别_场景文字识别Attention_飞桨-源于产业实践的开源深度学习平台...
  18. welsh-powell
  19. 懒惰的人生,注定一事无成
  20. Truck History prime

热门文章

  1. 中国锦鲤信小呆为兑奖曾刷爆信用卡、陷入焦虑?网友:但我仍想做锦鲤
  2. java httpresponse headres属性,Http Header里的Content-Type - 飞鸿影~ - 博客园
  3. 初探基于GameProtocol和NetFrame的RPG服务器
  4. 用python效率办公_如何用Python提高办公(Excel)效率?
  5. 安卓采集摄像头画面生成MP4文件
  6. linux目录结构与功能_深入理解linux系统的目录结构(总结的非常详细)
  7. webservice无法理解soap头action_数学是对理解的追求,而不仅仅是追求计算
  8. iptables学习笔记:使用NAT实现简单的无线AP
  9. 5u以太网用交换机连接电脑_干货丨如何用自己的电脑直接连接NUS打印机
  10. 【Flink】Flink Failed to push metrics to PushGateway Connect refuse