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字節之內,壓縮效果很是明顯。