HTTP cookies,通常稱之為“cookie”,已經存在很長時間了,但是仍然沒有被充分理解。首要問題是存在許多誤解,認為 cookie 是后門程序或病毒,卻忽視了其工作原理。第二個問題是,對于 cookie 的操作缺少統一的接口。盡管存在這些問題,cookie 仍舊在 Web 開發中扮演者重要的角色,以至于如果沒有出現相應的代替品就消失的話,我們許多喜歡的 Web 應用將變的不可用。
cookie 的起源
早期的 Web 應用面臨的最大問題之一就是如何維持狀態。簡言之,服務器無法知道兩個請求是否來自于同一個瀏覽器。當時,最簡單的辦法就是在請求的頁面中插入一個 token,然后在下次請求時將這個 token 返回至服務器。這需要在頁面的 form 表單中插入一個包含 token 的隱藏域,或者將 token 放在 URL 的 query 字符串中來傳遞。這兩種方法都需要手動操作,而且極易出錯。
cookie 是什么
簡單地說,cookie 就是瀏覽器儲存在用戶電腦上的一小段文本文件。cookie 是純文本格式,不包含任何可執行的代碼。一個 Web 頁面或服務器告知瀏覽器按照一定規范來儲存這些信息,并在隨后的請求中將這些信息發送至服務器,Web 服務器就可以使用這些信息來識別不同的用戶。大多數需要登錄的網站在用戶驗證成功之后都會設置一個 cookie,只要這個 cookie 存在并可以,用戶就可以自由瀏覽這個網站的任意頁面。再次說明,cookie 只包含數據,就其本身而言并不有害。
創建 cookie
Web 服務器通過發送一個稱為 Set-Cookie 的 HTTP 消息頭來創建一個 cookie,Set-Cookie消息頭是一個字符串,其格式如下(中括號中的部分是可選的):
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
消息頭的第一部分,value 部分,通常是一個 name=value格式的字符串。事實上,這種格式是原始規范中指定的格式,但是瀏覽器并不會對 cookie 值按照此格式來驗證。實際上,你可以指定一個不含等號的字符串,它同樣會被存儲。然而,最常用的使用方式是按照 name=value格式來指定 cookie 的值(大多數接口只支持該格式)。
當存在一個 cookie,并允許設置可選項,該 cookie 的值會在隨后的每次請求中被發送至服務器,cookie 的值被存儲在名為 Cookie 的 HTTP 消息頭中,并且只包含了 cookie 的值,忽略全部設置選項。例如:
Cookie: value
通過 Set-Cookie指定的可選項只會在瀏覽器端使用,而不會被發送至服務器端。發送至服務器的 cookie 的值與通過 Set-Cookie指定的值完全一樣,不會有進一步的解析或轉碼操作。如果請求中包含多個 cookie,它們將會被分號和空格分開,例如:
Cookie: value1; value2; name1=value1
服務器端框架通常包含解析 cookie 的方法,可以通過編程的方式獲取 cookie 的值。
cookie 編碼
對于 cookie 的值進行編碼一直都存在一些困惑。普遍認為 cookie 的值必須經過 URL 編碼,但其實這是一個謬論,盡管通常都這么做。原始規范中明確指出只有三個字符必須進行編碼:分號、逗號和空格,規范中還提到可以進行 URL 編碼,但并不是必須,在 RFC 中沒有提及任何編碼。然而,幾乎所有的實現都對 cookie 的值進行了一系列的 URL 編碼。對于 name=value格式,通常會對 name和 value 分別進行編碼,而不對等號 = 進行編碼操作。
過期時間選項
緊跟 cookie 值后面的每個選項都以分號和空格分開,每個選擇都指定了 cookie 在什么情況下應該被發送至服務器。第一個選項是過期時間(expires),指定了 cookie 何時不會再被發送至服務器,隨后瀏覽器將刪除該 cookie。該選項的值是一個 Wdy, DD-Mon-YYYY HH:MM:SS GMT 日期格式的值,例如:
Set-Cookie: name=Nicholas; expires=Sat, 02 May 2009 23:38:25 GMT
沒有設置 expires選項時,cookie 的生命周期僅限于當前會話中,關閉瀏覽器意味著這次會話的結束,所以會話 cookie 僅存在于瀏覽器打開狀態之下。這就是為什么為什么當你登錄一個 Web 應用時經常會看到一個復選框,詢問你是否記住登錄信息:如果你勾選了復選框,那么一個 expires 選項會被附加到登錄 cookie 中。如果 expires 設置了一個過去的時間點,那么這個 cookie 會被立即刪掉。
domain 選項
下一個選項是 domain,指定了 cookie 將要被發送至哪個或哪些域中。默認情況下,domain會被設置為創建該 cookie 的頁面所在的域名,所以當給相同域名發送請求時該 cookie 會被發送至服務器。例如,本博中 cookie 的默認值將是 bubkoo.com。domain 選項可用來擴充 cookie 可發送域的數量,例如:
Set-Cookie: name=Nicholas; domain=nczonline.net
像 Yahoo! 這種大型網站,都會有許多 name.yahoo.com 形式的站點(例如:my.yahoo.com, finance.yahoo.com 等等)。將一個 cookie 的 domain 選項設置為 yahoo.com,就可以將該 cookie 的值發送至所有這些站點。瀏覽器會把 domain 的值與請求的域名做一個尾部比較(即從字符串的尾部開始比較),并將匹配的 cookie 發送至服務器。
domain 選項的值必須是發送 Set-Cookie 消息頭的主機名的一部分,例如我不能在 google.com 上設置一個 cookie,因為這會產生安全問題。不合法的 domain
選擇將直接被忽略。
path 選項
另一個控制 Cookie消息頭發送時機的選項是 path選項,和 domain選項類似,path選項指定了請求的資源 URL 中必須存在指定的路徑時,才會發送Cookie 消息頭。這個比較通常是將 path 選項的值與請求的 URL 從頭開始逐字符比較完成的。如果字符匹配,則發送 Cookie 消息頭,例如:
Set-Cookie:name=Nicholas;path=/blog
在這個例子中,path 選項值會與 /blog,/blogrool 等等相匹配;任何以 /blog 開頭的選項都是合法的。需要注意的是,只有在 domain 選項核實完畢之后才會對 path 屬性進行比較。path 屬性的默認值是發送 Set-Cookie 消息頭所對應的 URL 中的 path 部分。
secure 選項
最后一個選項是 secure。不像其它選項,該選項只是一個標記而沒有值。只有當一個請求通過 SSL 或 HTTPS 創建時,包含 secure 選項的 cookie 才能被發送至服務器。這種 cookie 的內容具有很高的價值,如果以純文本形式傳遞很有可能被篡改,例如:
Set-Cookie: name=Nicholas; secure
事實上,機密且敏感的信息絕不應該在 cookie 中存儲或傳輸,因為 cookie 的整個機制原本都是不安全的。默認情況下,在 HTTPS 鏈接上傳輸的 cookie 都會被自動添加上 secure 選項。
Cookie 的維護和生命周期
在一個 cookie 中可以指定任意數量的選項,并且這些選項可以是任意順序,例如:
Set-Cookie:name=Nicholas; domain=nczonline.net; path=/blog
這個 cookie 有四個標識符:cookie 的 name,domain,path
,secure 標記。要想改變這個 cookie 的值,需要發送另一個具有相同 cookie name,domain,path 的 Set-Cookie 消息頭。例如:
Set-Cookie: name=Greg; domain=nczonline.net; path=/blog
這將覆蓋原來 cookie 的值。但是,修改 cookie 選項的任意一項都將創建一個完全不同的新 cookie,例如:
Set-Cookie: name=Nicholas; domain=nczonline.net; path=/
這個消息頭返回之后,會同時存在兩個名為 “name” 的不同的 cookie。如果你訪問 www.nczonline.net/blog 下的一個頁面,以下的消息頭將被包含進來:
Cookie: name=Greg; name=Nicholas
在這個消息頭中存在了兩個名為 “name” 的 cookie,path 值越詳細則 cookie 越靠前。 按照 domain-path-secure 的順序,設置越詳細的 cookie 在字符串中越靠前。假設我在 ww.nczonline.net/blog 下用默認選項創建了另一個 cookie:
Set-Cookie: name=Mike
那么返回的消息頭現在則變為:
Cookie: name=Mike; name=Greg; name=Nicholas
以 “Mike” 作為值的 cookie 使用了域名(www.nczonline.net
)作為其 domain 值并且以全路徑(/blog)作為其 path 值,則它較其它兩個 cookie 更加詳細。
使用失效日期
當 cookie 創建時指定了失效日期,這個失效日期則關聯了以 name-domain-path-secure 為標識的 cookie。要改變一個 cookie 的失效日期,你必須指定同樣的組合。當改變一個 cookie 的值時,你不必每次都設置失效日期,因為它不是 cookie 標識信息的組成部分。例如:
Set-Cookie:name=Mike;expires=Sat,03 May 2025 17:44:22 GMT
現在已經設置了 cookie 的失效日期,所以下次我想要改變 cookie 的值時,我只需要使用它的名字:
Set-Cookie:name=Matt
cookie 的失效日期并沒有改變,因為 cookie 的標識符是相同的。實際上,只有你手工的改變 cookie 的失效日期,否則其失效日期不會改變。這意味著在同一個會話中,一個會話 cookie 可以變成一個持久化 cookie(一個可以在多個會話中存在的),反之則不可。為了要將一個持久化 cookie 變為一個會話 cookie,你必須刪除這個持久化 cookie,這只要設置它的失效日期為過去某個時間之后再創建一個同名的會話 cookie 就可以實現。
需要記得的是失效日期是以瀏覽器運行的電腦上的系統時間為基準進行核實的。沒有任何辦法來來驗證這個系統時間是否和服務器的時間同步,所以當服務器時間和瀏覽器所處系統時間存在差異時這樣的設置會出現錯誤。
cookie 自動刪除
cookie 會被瀏覽器自動刪除,通常存在以下幾種原因:
會話 cooke (Session cookie) 在會話結束時(瀏覽器關閉)會被刪除
持久化 cookie(Persistent cookie)在到達失效日期時會被刪除
如果瀏覽器中的 cookie 數量達到限制,那么 cookie 會被刪除以為新建的 cookie 創建空間。
對于自動刪除來說,Cookie 管理顯得十分重要,因為這些刪除都是無意識的。
Cookie 限制條件
cookie 存在許多限制條件,來阻止 cookie 濫用并保護瀏覽器和服務器免受一些負面影響。有兩種 cookie 限制條件:cookie 的屬性和 cookie 的總大小。原始規范中限定每個域名下不超過 20 個 cookie,早期的瀏覽器都遵循該規范,并且在 IE7 中有更近一步的提升。在微軟的一次更新中,他們在 IE7 中增加 cookie 的限制數量到 50 個,與此同時 Opera 限定 cookie 數量為 30 個,Safari 和 Chrome 對與每個域名下的 cookie 個數沒有限制。
發向服務器的所有 cookie 的最大數量(空間)仍舊維持原始規范中所指出的:4KB。所有超出該限制的 cookie 都會被截掉并且不會發送至服務器。
Subcookies
鑒于 cookie 的數量存在限制,開發者提出 subcookies 的觀點來增加 cookie 的存儲量。Subcookies 是存儲在一個 cookie 值中的一些 name-value
對,通常與以下格式類似:
name=a=b&c=d&e=f&g=h
這種方式允許在單個 cookie 中保存多個 name-value 對,而不會超出瀏覽器 cookie 數量的限制。通過這種方式創建 cookie 的負面影響是,需要自定義解析方式來提取這些值,相比較而言 cookie 的格式會更為簡單。服務器端框架已開始支持 subcookies 的存儲。我編寫的 YUI Cookie utility,支持在 javascript 中讀/寫 subcookies
JavaScript 中的 cookie
在 JavaScript 中通過 document.cookie 屬性,你可以創建、維護和刪除 cookie。創建 cookie 時該屬性等同于 Set-Cookie 消息頭,而在讀取 cookie 時則等同于 Cookie 消息頭。在創建一個 cookie 時,你需要使用和 Set-Cookie 期望格式相同的字符串:
document.cookie="name=Nicholas;domain=nczonline.net;path=/";
設置 document.cookie 屬性的值并不會刪除存儲在頁面中的所有 cookie。它只簡單的創建或修改字符串中指定的 cookie。下次發送一個請求到服務器時,通過 document.cookie 設置的 cookie 會和其它通過 Set-Cookie 消息頭設置的 cookie 一并發送至服務器。這些 cookie 并沒有什么明確的不同之處。要使用 JavaScript 提取 cookie 的值,只需要從 document.cookie 中讀取即可。返回的字符串與 Cookie 消息頭中的字符串格式相同,所以多個 cookie 會被分號和字符串分割。例如:
name1=Greg; name2=Nicholas
鑒于此,你需要手工解析這個 cookie 字符串來提取真實的 cookie 數據。當前已有許多描述如何利用 JavaScript 來解析 cookie 的資料,包括我的書,Professional JavaScript,所以在這我就不再說明。通常利用已存在的 JavaScript 庫操作 cookie 會更簡單,如使用 YUI Cookie utility 來處理 cookie,而不要手工重新創建這些算法。
通過訪問 document.cookie 返回的 cookie 遵循發向服務器的 cookie 一樣的訪問規則。要通過 JavaScript 訪問 cookie,該頁面和 cookie 必須在相同的域中,有相同的 path,有相同的安全級別。
注意:一旦 cookie 通過 JavaScript 設置后便不能提取它的選項,所以你將不能知道 domain,path,expires 日期或 secure 標記。
HTTP-Only cookies
微軟的 IE6 SP1 在 cookie 中引入了一個新的選項:HTTP-only,HTTP-Only 背后的意思是告之瀏覽器該 cookie 絕不能通過 JavaScript 的 document.cookie 屬性訪問。設計該特征意在提供一個安全措施來幫助阻止通過 JavaScript 發起的跨站腳本攻擊 (XSS) 竊取 cookie 的行為(我會在另一篇博客中討論安全問題,本篇如此已足夠)。今天 Firefox2.0.0.5+、Opera9.5+、Chrome 都支持 HTTP-Only cookie。3.2 版本的 Safari 仍不支持。
要創建一個 HTTP-Only cookie,只要向你的 cookie 中添加一個 HTTP-Only
標記即可:
Set-Cookie: name=Nicholas; HttpOnly
一旦設定這個標記,通過 documen.coookie 則不能再訪問該 cookie。IE 同時更近一步并且不允許通過 XMLHttpRequest 的 getAllResponseHeaders()
或 getResponseHeader() 方法訪問 cookie,然而其它瀏覽器則允許此行為。Firefox 在 3.0.6 中修復了該漏洞,然而仍舊有許多瀏覽器漏洞存在,complete browser support list 列出了這些。
你不能通過 JavaScript 設置 HTTP-only
,因為你不能再通過 JavaScript 讀取這些 cookie,這是情理之中的事情。
總結
為了高效的利用 cookie,仍舊有許多要了解和弄明白的東西。對于一項創建于十多年前但仍舊如最初實現的那樣被使用至今的技術來說,這是件多不可思議的事。本篇只是提供了一些每個人都應該知道的關于瀏覽器 cookie 的基本指導,但無論如何,也不是一個完整的參考。對于今天的 Web 來說 cookie 仍舊起著非常重要的作用,并且不恰當的管理 cookie 會導致各種各樣的問題,從最糟糕的用戶體驗到安全漏洞。我希望這篇手冊能夠激起一些關于 cookie 的不可思議的亮點。