http://geek.csdn.net/news/detail/188003算法
HTTPS做为安全协议而诞生,那么就不得不面对如下两大安全问题:缓存
身份验证安全
确保通讯双方身份的真实性。直白一些,A但愿与B通讯,A如何确认B的身份不是由C伪造的。服务器
(由C伪造B的身份与A通讯,称为中间人攻击)网络
通讯加密并发
通讯的机密性、完整性依赖于算法与密钥,通讯双方是如何选择算法与密钥的。dom
能同时解决以上两个问题,就能确保真实有效的通讯双方采起有效的算法与密钥进行通讯,便完成了协议安全的初衷。源码分析
在介绍HTTPS协议如何解决两大安全问题前,咱们首先了解几个概念。性能
数字证书优化
数字证书是互联网通讯中标识双方身份信息的数字文件,由CA签发。
CA
CA(certification authority)是数字证书的签发机构。做为权威机构,其审核申请者身份后签发数字证书,这样咱们只须要校验数字证书便可肯定对方的真实身份。
HTTPS协议、SSL协议、TLS协议、握手协议的关系
HTTPS是Hypertext Transfer Protocol over Secure Socket Layer的缩写,即HTTP over SSL,可理解为基于SSL的HTTP协议。HTTPS协议安全是由SSL协议(目前经常使用的,本文基于TLS 1.2进行分析)实现的。
SSL协议是一种记录协议,扩展性良好,能够很方便的添加子协议,而握手协议即是SSL协议的一个子协议。
TLS协议是SSL协议的后续版本,本文中涉及的SSL协议默认是TLS协议1.2版本。
HTTPS协议的安全性由SSL协议实现,当前使用的TLS协议1.2版本包含了四个核心子协议:握手协议、密钥配置切换协议、应用数据协议及报警协议。
解决身份验证与通讯加密的核心,即是握手协议,接下来着重介绍握手协议。
握手协议的做用即是通讯双方进行身份确认、协商安全链接各参数(加密算法、密钥等),确保双方身份真实而且协商的算法与密钥可以保证通讯安全。
对握手协议的介绍限于客户端对服务端的身份验证,单向身份验证也是目前互联网公司最多见的认证方式。
首先咱们看一下协议交互,如图1所示:
接下来以Wireshark抓取接口的握手协议过程为例,针对每条协议消息分析。
ClientHello消息
ClientHello消息的做用是,将客户端可用于创建加密通道的参数集合,一次性发送给服务端。
消息内容包括:指望协议版本(TLS 1.2)、可供采用的密码套件(Cipher Suites)、客户端随机数(Random)及扩展字段内容(Extension)等信息,如图2所示。
ServerHello消息
ServerHello消息的做用是,在ClientHello参数集合中选择适合的参数,并将服务端用于创建加密通道的参数发送给客户端。
消息内容包括:采起的协议版本(TLS 1.2)、采用的密码套件(Cipher Suite)、服务端随机数(Random)、用于恢复会话的会话ID(Session ID)及扩展字段等信息,如图3所示。
自此客户端与服务端的协议版本、密码套件已经协商完毕。
这里服务端下发的会话ID可用于后续恢复会话。若客户端在ClientHello中携带了会话ID,而且服务端承认,则双方直接经过原主密钥生成一套新的密钥便可继续通讯。将两个网络往返下降为一个网络往返,提升通道创建的效率。
Certificate消息
Certificate消息的做用是,将服务端证书的详细信息发送给客户端,供客户端进行服务端身份校验。
消息内容:服务端下发的证书链,如图4所示。
服务端为了保证下发的证书可以被客户端正确识别,就须要将签发此证书的CA证书一同下发,构成证书链,保证客户端能够根据证书链的信息在系统配置中找到根证书,并经过根证书的公钥逐层向下验证证书的合法性。
如图所示,五八服务器下发了两个证书:本身的证书与签发CA的证书。经过签发CA的证书信息,可以直接找到根证书。
客户端本地校验服务端证书,若校验经过,则客户端对服务端的身份验证便完成了。
Certificate这个阶段解决了两端的身份验证问题。借助CA的力量,经过CA签发证书,将身份验证的工做交给了CA处理。
只要是咱们承认的CA,签发的证书咱们均承认证书持有者的身份。因为CA的介入,解决了中间人攻击的问题,由于中间人并无服务端的证书可供客户端验证。
ServerKeyExchange消息(可能不发送)
ServerKeyExchange消息的做用是,将须要服务端提供的密钥交换的额外参数,传给客户端。有的算法不须要额外参数,则ServerKeyExchange消息可不发送。
消息内容:用于密钥交换的额外参数,如图5所示。
如图5,服务端下发了“EC Diffile-Hellman”密钥交换算法所须要的参数。
ServerHelloDone消息
ServerHelloDone消息的做用是,通知客户端ServerHello阶段的数据均已发送完毕,等待客户端下一步消息。
ClientKeyExchange消息
ClientKeyExchange消息的做用是,将客户端须要为密钥交换提供的数据发送给服务端。
当咱们选用RSA密钥交换算法时,此消息的内容即是经过证书公钥加密的用于生成主密钥的预主密钥。
如图6所示,因为选用的密钥交换算法是“EC Diffie-Hellman”,因此ClientKeyExchange消息发送的是”EC Diffie-Hellman”算法须要的客户端参数。
当发送了ClientKeyExchange后,两端均具备了生成主密钥的完整密钥数据与随机数,两端分别根据所选算法计算主密钥便可。
至此,ClientKeyExchange发送后,两端都可生成主密钥,密钥交换问题便解决了。
有的读者可能对随机数的采用有些疑惑,笔者以为随机数的加入是为了提升密钥的随机性。
因为客户端直接生成的密钥颇有可能不够随机,而经过预主密钥加上两端提供的两个随机数作种子,建立的主密钥能够保证更加贴近真实随机的密钥。
ChangeCipherSpec消息
通过以上六条消息,咱们已经解决了身份认证问题、密码套件选取问题、密钥交换问题。双方也已经经过主密钥生成了实际使用的六个加解密密钥。
ChangeCipherSpec消息的做用,即是声明后续消息均采用密钥加密。在此消息后,咱们在WireShark上便看不到明文信息了。
Finished消息
Finished消息的做用,是对握手阶段全部消息计算摘要,并发送给对方校验,避免通讯过程当中被中间人所篡改。
自此,HTTPS如何保证通讯安全,经过握手协议的介绍,咱们已经有所了解。
可是,在全面使用HTTPS前,咱们还须要考虑一个众所周知的问题——HTTPS性能。
相对HTTP协议来讲,HTTPS协议创建数据通道的更加耗时,若直接部署到App中,势必下降数据传递的效率,间接影响用户体验。
接下来,介绍HTTPS性能救星——HTTP2协议。
随着互联网的快速发展,HTTP1.x协议获得了迅猛发展,但当App一个页面包含了数十个请求时,HTTP1.x协议的局限性便暴露了出来:
HTTP2正是为了解决HTTP1.x暴露出来的问题而诞生的。
说到HTTP2不得不提spdy。
因为HTTP1.x暴露出来的问题,Google设计了全新的名为spdy的新协议。spdy在五层协议栈的TCP层与HTTP层引入了一个新的逻辑层以提升效率。spdy是一个中间层,对TCP层与HTTP层有很好的兼容,不须要修改HTTP层便可改善应用数据传输速度。
spdy经过多路复用技术,使客户端与服务器只须要保持一条连接便可并发屡次数据交互,提升了通讯效率。
而HTTP2便士基于spdy的思路开发的。
经过流与帧概念的引入,继承了spdy的多路复用,并增长了一些实用特性。
HTTP2有什么特性呢?HTTP2的特性不只解决了上述已暴露的问题,还有一些功能使HTTP协议更加好用。
此外,HTTP2目前在实际使用中,只用于HTTPS协议场景下,经过握手阶段ClientHello与ServerHello的extension字段协商而来,因此目前HTTP2的使用场景,都是默认安全加密的。
下面介绍HTTP2协议协商以及多路复用与压缩头信息两大特性,实现部分采用okhttp源码(基于parent-3.4.2)进行分析与介绍。
okhttp是目前使用最普遍的支持HTTP2的Android端开源网络库,以okhttp为例介绍HTTP2特性也可方便读者提早了解okhttp,方便后续接入okhttp。
HTTP2协议的协商是在握手阶段进行的。
协商的方式是经过握手协议extension扩展字段进行扩展,新增Application Layer Protocol Negotiation字段进行协商。
在握手协议的ClientHello阶段,客户端将所支持的协议列表填入Application Layer Protocol Negotiation字段,供服务端进行挑选。如图7所示:
服务端收到ClientHello消息后,在客户端所支持的协议列表中选择适当协议做为后续应用层协议。如图8所示:
这样,两端便完成了HTTP2协议的协商。
在HTTP2未出现时,spdy也是经过扩展字段,扩展出next_protocol_negotiation字段,以NPN协议进行spdy的协商。不过因为NPN协议协商过于复杂,对https协议侵入性较强,在出现ALPN协商协议后,便逐渐被淘汰了。因此,本文协议协商并为对NPN协议协商作介绍。
http2为了优化http1.x对TCP性能的浪费,提出了多路复用的概念。
多路复用的含义
在HTTP2中,同一域名下的请求,可经过同一条TCP链路进行传输,使多个请求没必要单独创建链路,节省创建链路的开销。
为了达到这个目的,HTTP2提出了流与帧的概念,流表明请求与响应,而请求与响应具体的数据则包装为帧,对链路中传输的数据经过流ID与帧类型进行区分处理。图9即是多路复用的抽象图,每一个块表明一帧,而相同颜色的块则表明是同一个流。
那么HTTP2的多路复用是如何实现的呢?
因为网络请求的场景不少,咱们选择其中一个路径来介绍:
默认咱们已经添加各参数建立了Request对象r,并经过Request对象建立了Call对象c。并在独立线程中,调用c.execute()方法,进行同步请求操做。
okhttp调用execute方法后,其实是由一系列的interceptor来负责执行的。
interceptor根据添加顺序依此执行,其中咱们关注的是RetryAndFollowUpInterceptor、ConnectInterceptor0、CallServerInterceptor。
1.在RetryAndFollowUpInterceptor中,okhttp为咱们建立了一个StreamAllocation对象,StreamAllocation中含有基于url建立的Address对象。
Address类的url字段与Request类的url字段不一样,Address类的url字段不包括path与query字段,只含有scheme与authority部分,这点在进行Connection复用的equal操做时起了很大做用。
2.在ConnectInterceptor中,StreamAllocation对象的Address与链接池中每一个Connection对象的Address依次进行匹配,匹配成功并知足一些条件的Connection即可复用。基于匹配出的Connection建立Http2xStream,用于后续读写操做。
与链接池中Address匹配主要经过Address的url,url因为只含有scheme与authority因此可用于域名的匹配,这即是okhttp基于域名层面多路复用的基础。
实际上真正进行流读写操做的是FramedConnection与FramedStream,Connection与Http2xStream是抽象于具体操做的类,以方便上层使用。
3.在CallServerInterceptor中,Http2xStream建立FramedStream用于Request发送,并将FramedStream与对应的StreamID绑定缓存下来,以便Response到来时,可以根据StreamID索引到对应的FramedSteam进行后续操做。
在FramedStream发送完Request后,执行readResponseHeaders方法时进行调用了wait,将当前线程挂起。
并在FramedConnection读线程收到StreamID消息时,在缓存中查询FramedStream并将对应线程唤醒进行Response解码。
概括下okhttp的多路复用实现思路:
在笔者看来,HTTP2即是一个良好兼容http协议格式的自定义协议,经过Stream将数据分发到各请求,经过Frame将请求数据详细细分。
HTTP2为了解决HTTP1.x中头信息过大致使效率低下的问题,提出的解决方案即是压缩头部信息。具体的压缩方式,则引入了HPACK。
HPACK压缩算法是专门为HTTP2头部压缩服务的。为了达到压缩头部信息的目的,HPACK将头部字段缓存为索引,经过索引ID表明头部字段。客户端与服务端维护索引表,通讯过程当中尽量采用索引进行通讯,收到索引后查询索引表,才能解析出真正的头部信息。
HPACK索引表划分为动态索引表与静态索引表,动态索引表是HTTP2协议通讯过程当中两端动态维护的索引表,而静态索引表是硬编码进协议中的索引表。
做为分析HPACK压缩头信息的基础,须要先介绍HPACK对索引以及头部字符串的表示方式。
索引
索引以整型数字表示,因为HPACK须要考虑压缩与编解码问题,因此整型数字结构定义如图10所示:
类别标识
经过类别标识进行HPACK类别分类,指导后续编解码操做,常见的有1,01,01000000等八个类别。
首字节低位整型
首字节排除类别标识的剩余位,用于表示低位整型。若数值大于剩余位所能表示的容量,则须要后续字节表示高位整型。
结束标识
表示此字节是否为整型解析终止字节。
高位整型
字节余下7bit,用于填充整型高位。
“结束标识+高位整型”字节可能有0个、也有可能有多个,依据数据大小而定。
譬如,若想表示类别为1,索引为2,则使用10000010便可,不须要额外字节增长高位整型。
头部字符串须要显式声明长度,因此数据首字节由“类型标识+数据长度”组成。如图11所示:
类型标识
是否选用哈夫曼编码,1为选用,0为不选用,okhttp默认不选用哈夫曼编码。
数据长度
标识数据长度,采用上面提到的整型表示法表示。
数据内容
二进制数据。
解码实例
下面综合okhttp源码分析HPACK解码头部字段过程。
对编码部分感兴趣的读者,能够查阅RFC 7541或直接分析OkHttp源码。
当咱们须要解码头部字段时,首先解析头部字段首字节(HPACK头部字段首字节分为8个类别,摘选其中3个类别说明),首字节用于指导当前头部字段的解析规则:
1xxxxxxx
类别标识为1,表明收到一条K、V均为索引的头部字段。
K、V值:经过解析HPACK整型获取KV对的索引值,并根据索引值映射对应的头部原字段便可,压缩效率最高。
01xxxxxx
类别标识为01,表明收到一条K为索引、V为原字段,且须要加入动态索引表的头部字段。
K值:经过解析HPACK整型获取K值索引值,并经过索引值映射对应的头部原字段。
V值:经过解析HPACK字符串获取V值原字段。
获取K、V值后还需插入动态索引表中。
01000000
01000000表明收到一条K、V均为原字段,且须要加入动态索引表的头部字段。
K、V值:经过解析HPACK字符串获取K、V原字段,并插入动态索引表中。
还有不加入动态索引表、调整索引表大小等类别,这里就不展开了,感兴趣的能够看okhttp源码实现。
okhttp解析头信息的核心方法实现以下:
void readHeaders() throws IOException { while (!source.exhausted()) { int b = source.readByte() & 0xff; if (b == 0x80) { // 10000000 //类别标识为1,但索引为0 throw new IOException("index == 0"); } else if ((b & 0x80) == 0x80) { // 1NNNNNNN //类别为1,经过readIndexedHeader解析整型index。 int index = readInt(b, PREFIX_7_BITS); //经过index获取完整头部字段 readIndexedHeader(index - 1); } else if (b == 0x40) { // 01000000 //01000000表明KV均为原字段,解析字符串依次获取K值、V值,并插入动态表中 readLiteralHeaderWithIncrementalIndexingNewName(); } else if ((b & 0x40) == 0x40) { // 01NNNNNN //01xxxxxx表明K值为索引,V值为原字符串,依次解析整型index与字符串,并插入动态表中 int index = readInt(b, PREFIX_6_BITS); readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); } else if ((b & 0x20) == 0x20) { // 001NNNNN //类别为001,含义是更新动态列表容量 maxDynamicTableByteCount = readInt(b, PREFIX_5_BITS); if (maxDynamicTableByteCount < 0 || maxDynamicTableByteCount > headerTableSizeSetting) { throw new IOException("Invalid dynamic table size update " + maxDynamicTableByteCount); } adjustDynamicTableByteCount(); } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. //这个类别表明KV均为原字符串,依次解析字符串,并不对解析后的KV值插入动态表。 readLiteralHeaderWithoutIndexingNewName(); } else { // 000?NNNN - Ignore never indexed bit. //与上一类别相似,但K值为索引,V值为原字符串 int index = readInt(b, PREFIX_4_BITS); readLiteralHeaderWithoutIndexingIndexedName(index - 1); } } }
压缩效果
K值为“accept-encoding”、V值为“gzip, deflate”的头部字段在HTTP2中可经过索引值15代替,从而达到头部字段压缩的效果。
“accept-charset”头部字段则经过14表明头部K值,而Value值根据HPACK规则编码写入流中。
经过HPACK,一个头部字段变化较少的App,每一个头部字段将会缩减至4字节之内,压缩效果很是明显。