在前端開發中,總會探討到Cookie與Web Storage API的差別,這兩者的使用都是為了在客戶端儲存狀態──Cookie是因應HTTP無狀態(Stateless)的特性而生,而Storage API是HTML5規範的標準API。

HTTP與Cookie

談Cookie其實必須從後端開始,實作Web應用程式的開發者都應該知道,HTTP是個無狀態協定,這就有了一個問題:應用程式會有狀態,那麼,狀態應該儲存在哪?

由於早期的瀏覽器不能儲存狀態,使得這類型的任務落到了HTTP伺服器的後端程式。因此,1994年Netscape公司的Lou Montulli,為了解決客戶不想在網站儲存交易的部份狀態,所以,他提出了Cookie初始規格,並在Netscape 0.9beta實現Cookie的支援,而到了1995年,Internet Explorer 2也開始支援Cookie。

目前,Cookie的最新規範是2011年發布的RFC 6265,運作原理是透過HTTP標頭(Header),伺服端要設置Cookie的話,是透過「Set-Cookie」回應標頭,例如Set-Cookie: k1=v1,若有多個Cookie要設置,則使用多個Set-Cookie標頭;待瀏覽器收到標頭,接著會將鍵值儲存在檔案,在後續的請求中,使用請求標頭「Cookie」送出同一來源網站的Cookie;若有多個Cookie必須送出,會以分號串接,例如Cookie: k1=v1; k2=v2的形式。

由於等號、分號等是HTTP的保留字元,就如同請求參數,若Cookie要儲存的鍵值資料,包含這類符號或中文等字元,也必須進行URI編碼,再透過Set-Cookie或Cookie發送。

沒有設置屬性的Cookie會在關閉瀏覽器後失效,亦即所謂的Session Cookie,儲存Session ID的Cookie就是此類;而在JavaScript走紅之後,XSS攻擊也跟著多了起來,為了避免Session ID之類的重要資料被竊取,Cookie本身可以附加HttpOnly屬性,令Cookie僅被用於HTTP傳輸,而不被JavaScript讀取;若附加Secure屬性,客戶端只能在加密連線的情況下發送Cookie,而面對這類型的Cookie,JavaScript也無法讀取。

另外,還有Expires屬性,能指定Cookie過期時間,這可以用來實作自動登入,或者儲存客戶端本身經常使用的資訊;但是,沒有標頭或屬性可以直接刪除Cookie,如果想刪除Cookie,方式是在Expires指定一個過往的時間,例如:Thu, 01 Jan 1970 00:00:00 GMT,直接令Cookie過期,瀏覽器就會刪除Cookie;如果想知道更多屬性設置,可以察看MDN的〈HTTP cookies〉(https://mzl.la/2ZYwzCo)。

後端與前端Cookie

Cookie是為了在客戶端儲存狀態而生,狀態的設置與發送是基於HTTP標頭,在JavaScript未興起的年代,主要是由後端設置Cookie,不同的後端平臺會有不同的API,從最簡單的標頭發送與接收,到封裝HTTP標頭細節的Cookie API,不一而足。另一個Session API,基本上也是封裝了Cookie標頭,有的平臺甚至提供Flash之類的API,讓請求作用的週期為兩次請求之間,基本上也是透過Cookie操作實現。

等到瀏覽器可儲存Cookie後,運行於客戶端的JavaScript,若要儲存狀態,直接將資料儲存為Cookie不就得了?這就是document.cookie的作用,其設置與Set-Cookie標頭無關,只要對document.cookie設定'k1=v1'形式的字串,就會直接在瀏覽器儲存Cookie,若對相同來源伺服端發出請求,document.cookie設置的Cookie也會發送給伺服端。

若需要透過document.cookie設置Cookie屬性,格式與Set-Cookie時的要求相同,必要時,也可以覆寫伺服端設置的Cookie屬性;不過,原本就是為了避免被JavaScript讀取的HttpOnly屬性,不能透過document.cookie設置,類似地,Secure屬性的設置是無效的;想刪除某個Cookie,也是在設定document.cookie時指定一個過往的時間。

事實上,document.cookie很有趣,每設定一次就會產生新的Cookie,然而,若對document.cookie取值,並非取得最後設定的Cookie,而是個'k1=v1;k2=v2'格式的字串,其中包含了全部有效的Cookie(不包含伺服端設置了HttpOnly或Secure屬性的Cookie)。

然而,document.cookie是原生操作Cookie的唯一管道,使用起來並不方便,還要考慮URI編碼特定URI保留字元的問題,因此,最好的方式是將細節封裝起來,在MDN的〈Document.cookie〉就提供一個實作參考,我們可以透過函式,來進行Cookie的寫入、走訪、讀取、刪除等任務,如果懶得自己寫,也可以使用js-cookie這類現成的程式庫。

Web Storage API

在早期的Web應用程式開發中,Cookie是唯一可在客戶端儲存狀態的通用方案,但它有兩大問題。

首先,單一Cookie的容量不大(主要是由於HTTP標頭的字串長度有限),每個來源可設置的Cookie總數不多,雖說因各瀏覽器而不同,然而保險的假設通常是4KB的容量,約可設定50個Cookie。

另一個問題是,Cookie是因應HTTP無狀態特性而生,用於每次對伺服端請求時主動告知客戶端的狀態;然而,就算儲存的狀態只會在客戶端使用,與伺服端的處理無關,只要來源相同,每次HTTP請求時,其實都會附上Cookie。即便請求對象只是網頁引用的外部圖片、CSS、JavaScript檔案等,也都會附上Cookie。所以,若網頁引用的外部資源很多,因重複發送的Cookie而耗費的流量,就很可觀了。

到了HTML5,當中規範了Web Storage API,我們可以透過sessionStorage或localStorage,管理瀏覽器提供的儲存空間,大小約5MB(因瀏覽器而異)。前者儲存的資料,在同一會話階段期間有效,後者可以長期保留。而且,Storage API與HTTP沒有關係,純粹用來儲存客戶端的狀態,因此,若不用在每次請求中發送給伺服端的狀態,就可以使用Storage API來儲存。

在API的便利性上,sessionStorage或localStorage都是Storage的實例,當中提供了setItem、getItem、removeItem、clear等方法,也可以透過[]運算子來設置或取得資料,因此,也能夠透過Object.keys來取得鍵清單等,Storage實例也支援storage事件,可用來監聽鍵值的變化,相對於使用document.cookie來說,操作上便利許多;原生的localStorage不提供過期時間的設定,不過,我們可以自行實現,在儲存時加上時間戳記,並在存取資料時,與當前時間戳記相比,看看是否逾時,從而決定是否刪除資料。

HTTP特性與安全性

不少開發者喜歡用容量差異來區別Cookie與Storage API,不過,HTTP的特性才是重點之一。

舉例來說,對於不用每次對伺服端請求時,都要主動告知的客戶端狀態,當下若使用Storage API,會比較適合;相對地,需要伺服端控制相關的情境,像Session ID、自動登入的控管、加密傳送的資訊等,此時,就該考量Cookie。

同時,安全性會是最重要的考量。因為Storage API沒有Cookie的HttpOnly屬性之類的設定,JavaScript可以任意讀取,所以,最常引發的疑慮就是對XSS抵擋能力脆弱。當然,就算使用Cookie,也不建議儲存敏感資料就是了。關於這部份的作法,W3C有份〈Web Storage〉(https://bit.ly/2KzIZvf)的使用建議,其中也有針對安全的探討,建議參考看看!

作者簡介


Advertisement

更多 iThome相關內容