1. 程式人生 > >Cookie/Session的機制與安全

Cookie/Session的機制與安全

Cookie和Session是為了在無狀態的HTTP協議之上維護會話狀態,使得伺服器可以知道當前是和哪個客戶在打交道。本文來詳細討論Cookie和Session的實現機制,以及其中涉及的安全問題。

因為HTTP協議是無狀態的,即每次使用者請求到達伺服器時,HTTP伺服器並不知道這個使用者是誰、是否登入過等。現在的伺服器之所以知道我們是否已經登入,是因為伺服器在登入時設定了瀏覽器的Cookie!Session則是藉由Cookie而實現的更高層的伺服器與瀏覽器之間的會話。

Cookie是由網景公司的前僱員Lou Montulli在1993年發明的,現今Cookie已經廣泛使用了。

Cookie 的實現機制

Cookie是由客戶端儲存的小型文字檔案,其內容為一系列的鍵值對。 Cookie是由HTTP伺服器設定的,儲存在瀏覽器中, 在使用者訪問其他頁面時,會在HTTP請求中附上該伺服器之前設定的Cookie。 Cookie的實現標準定義在RFC2109: HTTP State Management Mechanism中。 那麼Cookie是怎樣工作的呢?下面給出整個Cookie的傳遞流程:

  1. 瀏覽器向某個URL發起HTTP請求(可以是任何請求,比如GET一個頁面、POST一個登入表單等)
  2. 對應的伺服器收到該HTTP請求,並計算應當返回給瀏覽器的HTTP響應。

    HTTP響應包括請求頭和請求體兩部分,可以參見:

    讀 HTTP 協議

  3. 在響應頭加入Set-Cookie欄位,它的值是要設定的Cookie。

    RFC2109 6.3 Implementation Limits中提到: UserAgent(瀏覽器就是一種使用者代理)至少應支援300項Cookie, 每項至少應支援到4096位元組,每個域名至少支援20項Cookie。

  4. 瀏覽器收到來自伺服器的HTTP響應。

  5. 瀏覽器在響應頭中發現Set-Cookie欄位,就會將該欄位的值儲存在記憶體或者硬碟中。

    Set-Cookie欄位的值可以是很多項Cookie,每一項都可以指定過期時間Expires。 預設的過期時間是使用者關閉瀏覽器時。

  6. 瀏覽器下次給該伺服器傳送HTTP請求時, 會將伺服器設定的Cookie附加在HTTP請求的頭欄位Cookie

    中。

    瀏覽器可以儲存多個域名下的Cookie,但只發送當前請求的域名曾經指定的Cookie, 這個域名也可以在Set-Cookie欄位中指定)。

  7. 伺服器收到這個HTTP請求,發現請求頭中有Cookie欄位, 便知道之前就和這個使用者打過交道了。

  8. 過期的Cookie會被瀏覽器刪除。

總之,伺服器通過Set-Cookie響應頭欄位來指示瀏覽器儲存Cookie, 瀏覽器通過Cookie請求頭欄位來告訴伺服器之前的狀態。 Cookie中包含若干個鍵值對,每個鍵值對可以設定過期時間。

Cookie 的安全隱患

Cookie提供了一種手段使得HTTP請求可以附加當前狀態, 現今的網站也是靠Cookie來標識使用者的登入狀態的:

  1. 使用者提交使用者名稱和密碼的表單,這通常是一個POST HTTP請求。
  2. 伺服器驗證使用者名稱與密碼,如果合法則返回200(OK)並設定Set-Cookieauthed=true
  3. 瀏覽器儲存該Cookie。
  4. 瀏覽器傳送請求時,設定Cookie欄位為authed=true
  5. 伺服器收到第二次請求,從Cookie欄位得知該使用者已經登入。 按照已登入使用者的許可權來處理此次請求。

這裡面的問題在哪裡?

我們知道可以傳送HTTP請求的不只是瀏覽器,很多HTTP客戶端軟體(包括curl、Node.js)都可以傳送任意的HTTP請求,可以設定任何頭欄位。 假如我們直接設定Cookie欄位為authed=true併發送該HTTP請求, 伺服器豈不是被欺騙了?這種攻擊非常容易,Cookie是可以被篡改的!

Cookie 防篡改機制

伺服器可以為每個Cookie項生成簽名,由於使用者篡改Cookie後無法生成對應的簽名, 伺服器便可以得知使用者對Cookie進行了篡改。一個簡單的校驗過程可能是這樣的:

  1. 在伺服器中配置一個不為人知的字串(我們叫它Secret),比如:x$sfz32
  2. 當伺服器需要設定Cookie時(比如authed=false),不僅設定authed的值為false, 在值的後面進一步設定一個簽名,最終設定的Cookie是authed=false|6hTiBl7lVpd1P
  3. 簽名6hTiBl7lVpd1P是這樣生成的:Hash('x$sfz32'+'true')。 要設定的值與Secret相加再取雜湊。
  4. 使用者收到HTTP響應並發現頭欄位Set-Cookie: authed=false|6hTiBl7lVpd1P
  5. 使用者在傳送HTTP請求時,篡改了authed值,設定頭欄位Cookie: authed=true|???。 因為使用者不知道Secret,無法生成簽名,只能隨便填一個。
  6. 伺服器收到HTTP請求,發現Cookie: authed=true|???。伺服器開始進行校驗: Hash('true'+'x$sfz32'),便會發現使用者提供的簽名不正確。

通過給Cookie添加簽名,使得伺服器得以知道Cookie被篡改。然而故事並未結束。

因為Cookie是明文傳輸的, 只要伺服器設定過一次authed=true|xxxx我不就知道true的簽名是xxxx了麼, 以後就可以用這個簽名來欺騙伺服器了。因此Cookie中最好不要放敏感資料。 一般來講Cookie中只會放一個Session Id,而Session儲存在伺服器端。

Session 的實現機制

Session 是儲存在伺服器端的,避免了在客戶端Cookie中儲存敏感資料。 Session 可以儲存在HTTP伺服器的記憶體中,也可以存在記憶體資料庫(如redis)中, 對於重量級的應用甚至可以儲存在資料庫中。

我們以儲存在redis中的Session為例,還是考察如何驗證使用者登入狀態的問題。

  1. 使用者提交包含使用者名稱和密碼的表單,傳送HTTP請求。
  2. 伺服器驗證使用者發來的使用者名稱密碼。
  3. 如果正確則把當前使用者名稱(通常是使用者物件)儲存到redis中,並生成它在redis中的ID。

    這個ID稱為Session ID,通過Session ID可以從Redis中取出對應的使用者物件, 敏感資料(比如authed=true)都儲存在這個使用者物件中。

  4. 設定Cookie為sessionId=xxxxxx|checksum併發送HTTP響應, 仍然為每一項Cookie都設定簽名。

  5. 使用者收到HTTP響應後,便看不到任何敏感資料了。在此後的請求中傳送該Cookie給伺服器。

  6. 伺服器收到此後的HTTP請求後,發現Cookie中有SessionID,進行放篡改驗證。

  7. 如果通過了驗證,根據該ID從Redis中取出對應的使用者物件, 檢視該物件的狀態並繼續執行業務邏輯。

Web應用框架都會實現上述過程,在Web應用中可以直接獲得當前使用者。 相當於在HTTP協議之上,通過Cookie實現了持久的會話。這個會話便稱為Session。