HTTP 必知必會的那些

玉剛說發表於2019-03-04

本文由玉剛說寫作平臺提供寫作贊助

原作者:竹千代

版權宣告:本文版權歸微信公眾號玉剛說所有,未經許可,不得以任何形式轉載

Http是我們經常打交道的網路應用層協議,它的重要性可能不需要再強調。但是實際上很多人,包括我自己可能對http瞭解的並不夠深。本文就我自己的學習心得,分享一下我認為需要知道的快取所涉及到的相關知識點。

Http報文

首先我們來點基礎的,看看http報文具體的格式。http報文可以分為請求報文和響應報文,格式大同小異。主要分為三個部分:

  1. 起始行
  2. 首部
  3. 主體

請求報文格式:

<method> <request-url> <version>
<headers>

<entity-body>
複製程式碼

響應報文格式

<version> <status> <reason-phrase>
<headers>

<entity-body>
複製程式碼

從請求報文格式和響應報文格式可以看出,兩者主要在起始行上有差異。這裡稍微解釋一下各個標籤:

<method> 指請求方法,常用的主要是Get、 Post、Head 還有其他一些我們這裡就不說了,有興趣的可以自己查閱一下

<version> 指協議版本,現在通常都是Http/1.1了

<request-url> 請求地址

<status> 指響應狀態碼, 我們熟悉的200、404等等

<reason-phrase> 原因短語,200 OK 、404 Not Found 這種後面的描述就是原因短語,通常不必太關注。
複製程式碼

method

我們知道請求方法最常用的有Get 和Post兩種,面試時也常常會問到這兩者有什麼區別,通常什麼情況下使用。這裡我們來簡單說一說。

兩個方法之間在傳輸形式上有一些區別,通過Get方法發起請求時,會將請求引數拼接在request-url尾部,格式是url?param1=xxx&param2=xxx&[...]。

我們需要知道,這樣傳輸引數會使得引數都暴露在位址列中。並且由於url是ASCII編碼的,所以引數中如果有Unicode編碼的字元,例如漢字,都會編碼之後傳輸。另外值得注意的是,雖然http協議並沒有對url長度做限制,但是一些瀏覽器和伺服器可能會有限制,所以通過GET方法發起的請求引數不能夠太長。而通過POST方法發起的請求是將引數放在請求體中的,所以不會有GET引數的這些問題。

另外一點差別就是方法本身的語義上的。GET方法通常是指從伺服器獲取某個URL資源,其行為可以看作是一個讀操作,對同一個URL進行多次GET並不會對伺服器產生什麼影響。而POST方法通常是對某個URL進行新增、修改,例如一個表單提交,通常會往伺服器插入一條記錄。多次POST請求可能導致伺服器的資料庫中新增了多條記錄。所以從語義上來講,兩者也是不能混為一談的。

狀態碼

常見的狀態碼主要有
200 OK 請求成功,實體包含請求的資源
301 Moved Permanent 請求的URL被移除了,通常會在Location首部中包含新的URL用於重定向。
304 Not Modified 條件請求進行再驗證,資源未改變。
404 Not Found 資源不存在
206 Partial Content 成功執行一個部分請求。這個在用於斷點續傳時會涉及到。

header

在請求報文和響應報文中都可以攜帶一些資訊,通過與其他部分配合,能夠實現各種強大的功能。這些資訊位於起始行之下與請求實體之間,以鍵值對的形式,稱之為首部。每條首部以回車換行符結尾,最後一個首部額外多一個換行,與實體分隔開。

這裡我們重點關注一下
Date
Cache-Control
Last-Modified
Etag
Expires
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match

Http的首部還有很多,但限於篇幅我們不一一討論。這些首部都是Http快取會涉及到的,在下文中我們會來說說各自的作用。

實體

請求傳送的資源,或是響應返回的資源。

Http快取

當我們發起一個http請求後,伺服器返回所請求的資源,這時我們可以將該資源的副本儲存在本地,這樣當再次對該url資源發起請求時,我們能快速的從本地儲存裝置中獲取到該url資源,這就是所謂的快取。快取既可以節約不必要的網路頻寬,又能迅速對http請求做出響應。

先擺出幾個概念:

  1. 新鮮度檢測
  2. 再驗證
  3. 再驗證命中

我們知道,有些url所對應的資源並不是一成不變的,伺服器中該url的資源可能在一定時間之後會被修改。這時本地快取中的資源將與伺服器一側的資源有差異。

既然在一定時間之後可能資源會改變,那麼在某個時間之前我們可以認為這個資源沒有改變,從而放心大膽的使用快取資源,當請求時間超過來該時間,我們認為這個快取資源可能不再與伺服器端一致了。所以當我們發起一個請求時,我們需要先對快取的資源進行判斷,看看究竟我們是否可以直接使用該快取資源,這個就叫做新鮮度檢測。即每個資源就像一個食品一樣,擁有一個過期時間,我們吃之前需要先看看有沒有過期。

如果發現該快取資源已經超過了一定的時間,我們再次發起請求時不會直接將快取資源返回,而是先去伺服器檢視該資源是否已經改變,這個就叫做再驗證。如果伺服器發現對應的url資源並沒有發生變化,則會返回304 Not Modified,並且不再返回對應的實體。這稱之為再驗證命中。相反如果再驗證未命中,則返回200 OK,並將改變後的url資源返回,此時快取可以更新以待之後請求。

我們看看具體的實現方式:

  1. 新鮮度檢測
    我們需要通過檢測資源是否超過一定的時間,來判斷快取資源是否新鮮可用。那麼這個一定的時間怎麼決定呢?其實是由伺服器通過在響應報文中增加Cache-Control:max-age,或是Expire這兩個首部來實現的。值得注意的是Cache-Control是http1.1的協議規範,通常是接相對的時間,即多少秒以後,需要結合last-modified這個首部計算出絕對時間。而Expire是http1.0的規範,後面接一個絕對時間。
  1. 再驗證
    如果通過新鮮度檢測發現需要請求伺服器進行再驗證,那麼我們至少需要告訴伺服器,我們已經快取了一個什麼樣的資源了,然後伺服器來判斷這個快取資源到底是不是與當前的資源一致。邏輯是這樣沒錯。那怎麼告訴伺服器我當前已經有一個備用的快取資源了呢?我們可以採用一種稱之為條件請求的方式實現再驗證。
  1. Http定義了5個首部用於條件請求:
    If-Modified-Since
    If-None-Match
    If-Unmodified-Since
    If-Range
    If-Match

If-Modified-Since 可以結合Last-Modified這個伺服器返回的響應首部使用,當我們發起條件請求時,將Last-Modified首部的值作為If-Modified-Since首部的值傳遞到伺服器,意思是查詢伺服器的資源自從我們上一次快取之後是否有修改。

If-None-Match 需要結合另一個Etag的伺服器返回的響應首部使用。Etag首部實際上可以認為是伺服器對文件資源定義的一個版本號。有時候一個文件被修改了,可能所做的修改極為微小,並不需要所有的快取都重新下載資料。或者說某一個文件的修改週期極為頻繁,以至於以秒為時間粒度的判斷已經無法滿足需求。這個時候可能就需要Etag這個首部來表明這個文件的版號了。發起條件請求時可將快取時儲存下來的Etag的值作為If-None-Match首部的值傳送至伺服器,如果伺服器的資源的Etag與當前條件請求的Etag一致,表明這次再驗證命中。
其他三個與斷點續傳涉及到的相關知識有關,本文暫時不討論。待我之後寫一篇文章來講講斷點續傳。

OkHttp的快取

快取的Http理論知識大致就是這麼些。我們從OkHttp的原始碼來看看,這些知名的開源庫是如何利用Http協議實現快取的。這裡我們假設讀者對OkHttp的請求執行流程有了大致的瞭解,並且只討論快取相關的部分。對於OkHttp程式碼不熟悉的同學,建議先看看相關程式碼或是其他文章。

我們知道OkHttp的請求在傳送到伺服器之前會經過一系列的Interceptor,其中有一個CacheInterceptor即是我們需要分析的程式碼。

 final InternalCache cache;

@Override public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    ......
     
 }
複製程式碼

方法首先通過InternalCache 獲取到對應請求的快取。這裡我們不展開討論這個類的具體實現,只需要知道,如果之前快取了該請求url的資源,那麼通過request物件可以查詢到這個快取響應。

將獲取到的快取響應,當前時間戳和請求傳入CacheStrategy,然後通過執行get方法執行一些邏輯最終可以獲取到strategy.networkRequest,strategy.cacheResponse。如果通過CacheStrategy的判斷之後,我們發現這次請求無法直接使用快取資料,需要向伺服器發起請求,那麼我們就通過CacheStrategy為我們構造的networkRequest來發起這次請求。我們先來看看CacheStrategy做了哪些事情。

CacheStrategy.Factory.java

public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

複製程式碼

CacheStrategy.Factory的構造方法首先儲存了傳入的引數,並將快取響應的相關首部解析儲存下來。之後呼叫的get方法如下


    
    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
    }

複製程式碼

get方法很簡單,主要邏輯在getCandidate中,這裡的邏輯是如果返回的candidate所持有的networkRequest不為空,表示我們這次請求需要發到伺服器,此時如果請求的cacheControl要求本次請求只使用快取資料。那麼這次請求恐怕只能以失敗告終了,這點我們等會兒回到CacheInterceptor中可以看到。接著我們看看主要getCandidate的主要邏輯。

    private CacheStrategy getCandidate() {
      // No cached response.
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // If this response shouldn't have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
        ......
    }
    
複製程式碼

上面這段程式碼主要列出四種情況下需要忽略快取,直接想伺服器發起請求的情況:

  1. 快取本身不存在
  2. 請求是採用https 並且快取沒有進行握手的資料。
  3. 快取本身不應該不儲存下來。可能是快取本身實現有問題,把一些不應該快取的資料保留了下來。
  4. 如果請求本身新增了 Cache-Control: No-Cache,或是一些條件請求首部,說明請求不希望使用快取資料。

這些情況下直接構造一個包含networkRequest,但是cacheResponse為空的CacheStrategy物件返回。

    private CacheStrategy getCandidate() {
      ......
    
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }

      long ageMillis = cacheResponseAge();
      long freshMillis = computeFreshnessLifetime();

      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }




      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        return new CacheStrategy(null, builder.build());
      }
        
        ......     
    }
複製程式碼

如果快取響應的Cache-Control首部包含immutable,那麼說明該資源不會改變。客戶端可以直接使用快取結果。值得注意的是immutable並不屬於http協議的一部分,而是由facebook提出的擴充套件屬性。

之後分別計算ageMills、freshMills、minFreshMills、maxStaleMills這四個值。
如果響應快取沒有通過Cache-Control:No-Cache 來禁止客戶端使用快取,並且

ageMillis + minFreshMillis < freshMillis + maxStaleMillis
複製程式碼

這個不等式成立,那麼我們進入條件程式碼塊之後最終會返回networkRequest為空,並且使用當前快取值構造的CacheStrtegy。

這個不等式究竟是什麼含義呢?我們看看這四個值分別代表什麼。
ageMills 指這個快取資源自響應報文在源伺服器中產生或者過期驗證的那一刻起,到現在為止所經過的時間。用食品的保質期來比喻的話,好比當前時間距離生產日期已經過去了多久了。

freshMills 表示這個資源在多少時間內是新鮮的。也就是假設保質期18個月,那麼這個18個月就是freshMills。

minFreshMills 表示我希望這個快取至少在多久之後依然是新鮮的。好比我是一個比較講究的人,如果某個食品只有一個月就過期了,雖然並沒有真的過期,但我依然覺得食品不新鮮從而不想再吃了。

maxStaleMills好比我是一個不那麼講究的人,即使食品已經過期了,只要不是過期很久了,比如2個月,那我覺得問題不大,還可以吃。

minFreshMills 和maxStatleMills都是由請求首部取出的,請求可以根據自己的需要,通過設定

Cache-Control:min-fresh=xxx、Cache-Control:max-statle=xxx
複製程式碼

來控制快取,以達到對快取使用嚴格性的收緊與放鬆。

    private CacheStrategy getCandidate() {
        ......

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      String conditionName;
      String conditionValue;
      if (etag != null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }

      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }
複製程式碼

如果之前的條件不滿足,說明我們的快取響應已經過期了,這時我們需要通過一個條件請求對伺服器進行再驗證操作。接下來的程式碼比較清晰來,就是通過從快取響應中取出的Last-Modified,Etag,Date首部構造一個條件請求並返回。

接下來我們返回CacheInterceptor

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }
複製程式碼

可以看到,如果我們返回的networkRequestcacheResponse都為空,說明我們即沒有可用的快取,同時請求通過Cache-Control:only-if-cached只允許我們使用當前的快取資料。這個時候我們只能返回一個504的響應。接著往下看,

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
複製程式碼

如果networkRequest為空,說明我們不需要進行再驗證了,直接將cacheResponse作為請求結果返回。

Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    
    
     Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
複製程式碼

如果networkRequest存在不為空,說明這次請求是需要發到伺服器的。此時有兩種情況,一種cacheResponse不存在,說明我們沒有一個可用的快取,這次請求只是一個普通的請求。如果cacheResponse存在,說明我們有一個可能過期了的快取,此時networkRequest是一個用來進行再驗證的條件請求。

不管哪種情況,我們都需要通過networkResponse=chain.proceed(networkRequest)獲取到伺服器的一個響應。不同的只是如果有快取資料,那麼在獲取到再驗證的響應之後,需要cache.update(cacheResponse, response)去更新當前快取中的資料。如果沒有快取資料,那麼判斷此次請求是否可以被快取。在滿足快取的條件下,將響應快取下來,並返回。

OkHttp快取大致的流程就是這樣,我們從中看出,整個流程是遵循了Http的快取流程的。最後我們總結一下快取的流程:

  1. 從接收到的請求中,解析出Url和各個首部。
  2. 查詢本地是否有快取副本可以使用。
  3. 如果有快取,則進行新鮮度檢測,如果快取足夠新鮮,則使用快取作為響應返回,如果不夠新鮮了,則構造條件請求,發往伺服器再驗證。如果沒有快取,就直接將請求發往伺服器。
  4. 把從伺服器返回的響應,更新或是新增到快取中。

OAuth

OAuth是一個用於授權第三方獲取相應資源的協議。與以往的授權方式不同的是,OAuth的授權能避免使用者暴露自己的使用者密碼給第三方,從而更加的安全。OAuth協議通過設定一個授權層,以區分使用者和第三方應用。使用者本身可以通過使用者密碼登陸服務提供商,獲取到賬戶所有的資源。而第三方應用只能通過向使用者請求授權,獲取到一個Access Token,用以登陸授權層,從而在指定時間內獲取到使用者授權訪問的部分資源。

OAuth定義的幾個角色:

Role Description
Resource Owner 可以授權訪問某些受保護資源的實體,通常就是指使用者
Client 可以通過使用者的授權訪問受保護資源的應用,也就是第三方應用
Authorization server 在認證使用者之後給第三方下發Access Token的伺服器
Resource Server 擁有受保護資源的伺服器,可以通過Access Token響應資源請求
     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+
複製程式碼

從上圖可以看出,一個OAuth授權的流程主要可以分為6步:

  1. 客戶端向使用者申請授權。
  2. 使用者同意授權。
  3. 客戶端通過獲取的授權,向認證伺服器申請Access Token。
  4. 認證伺服器通過授權認證後,下發Access Token。
  5. 客戶端通過獲取的到Access Token向資源伺服器發起請求。
  6. 資源伺服器核對Access Token後下發請求資源。

Https

簡單的說 Http + 加密 + 認證 + 完整性保護 = Https

傳統的Http協議是一種應用層的傳輸協議,Http直接與TCP協議通訊。其本身存在一些缺點:

  1. Http協議使用明文傳輸,容易遭到竊聽。
  2. Http對於通訊雙方都沒有進行身份驗證,通訊的雙方無法確認對方是否是偽裝的客戶端或者服務端。
  3. Http對於傳輸內容的完整性沒有確認的辦法,往往容易在傳輸過程中被劫持篡改。

因此,在一些需要保證安全性的場景下,比如涉及到銀行賬戶的請求時,Http無法抵禦這些攻擊。
Https則可以通過增加的SSL\TLS,支援對於通訊內容的加密,以及對通訊雙方的身份進行驗證。

Https的加密

近代密碼學中加密的方式主要有兩類:

  1. 對稱祕鑰加密
  2. 非對稱祕鑰加密

對稱祕鑰加密是指加密與解密過程使用同一把祕鑰。這種方式的優點是處理速度快,但是如何安全的從一方將祕鑰傳遞到通訊的另一方是一個問題。

非對稱祕鑰加密是指加密與解密使用兩把不同的祕鑰。這兩把祕鑰,一把叫公開祕鑰,可以隨意對外公開。一把叫私有祕鑰,只用於本身持有。得到公開祕鑰的客戶端可以使用公開祕鑰對傳輸內容進行加密,而只有私有祕鑰持有者本身可以對公開祕鑰加密的內容進行解密。這種方式克服了祕鑰交換的問題,但是相對於對稱祕鑰加密的方式,處理速度較慢。

SSL\TLS的加密方式則是結合了兩種加密方式的優點。首先採用非對稱祕鑰加密,將一個對稱祕鑰使用公開祕鑰加密後傳輸到對方。對方使用私有祕鑰解密,得到傳輸的對稱祕鑰。之後雙方再使用對稱祕鑰進行通訊。這樣即解決了對稱祕鑰加密的祕鑰傳輸問題,又利用了對稱祕鑰的高效率來進行通訊內容的加密與解密。

Https的認證

SSL\TLS採用的混合加密的方式還是存在一個問題,即怎麼樣確保用於加密的公開祕鑰確實是所期望的伺服器所分發的呢?也許在收到公開祕鑰時,這個公開祕鑰已經被別人篡改了。因此,我們還需要對這個祕鑰進行認證的能力,以確保我們通訊的對方是我們所期望的物件。

目前的做法是使用由數字證照認證機構頒發的公開祕鑰證照。伺服器的運營人員可以向認證機構提出公開祕鑰申請。認證機構在稽核之後,會將公開祕鑰與共鑰證照繫結。伺服器就可以將這個共鑰證照下發給客戶端,客戶端在收到證照後,使用認證機構的公開祕鑰進行驗證。一旦驗證成功,即可知道這個祕鑰是可以信任的祕鑰。

總結 Https的通訊流程:

  1. Client發起請求
  2. Server端響應請求,並在之後將證照傳送至Client
  3. Client使用認證機構的共鑰認證證照,並從證照中取出Server端共鑰。
  4. Client使用共鑰加密一個隨機祕鑰,並傳到Server
  5. Server使用私鑰解密出隨機祕鑰
  6. 通訊雙方使用隨機祕鑰最為對稱祕鑰進行加密解密。
    HTTP 必知必會的那些
    歡迎關注我的微信公眾號,接收第一手技術乾貨

相關文章