最近,清除cookie的時候,使用了document.cookie = ''
,然后就發現沒有作用。于是,帶著上述疑問,找個時間研究了下cookie。本文主要是基于我遇到問題,然后解決問題的思路寫的,并不是以從0到1講述cookie的思路寫的。
為了解決上述問題,以及在解決問題中的發散思考,主要通過以下問題:
document.cookie = ''
能否用來清除cookie;- 同時設置了expires和max-age,哪個有效;
- 假設已經有一個名為hello的cookie,再次設置名為hello的cookie是新增還是修改;
- 如何把字符串形式的cookie改成json的形式;
- 如果設置了多個同名cookie,獲取cookie時以哪個為準;
學習了以下知識點:
- cookie賦值語法;
- cookie賦值算法;
- cookie取值算法。
預備知識
本文參考了英文文檔,所以提前說明以下英文對應的中文翻譯:
- cookie store: cookie表,瀏覽器內部用于存儲cookie信息的數據結構;
- cookie name: 一個cookie的名稱;
- cookie value: 一個cookie的值;
- cookie attribute: 一個cookie的屬性,cookie除了基本的名稱和值之外,還有用來進一步描述cookie的屬性,比如Expires,Max-Age,Domain,Path,Secure HttpOnly等。這些屬性是提供給用戶設置cookie的時候使用的,并不是cookie內部實際存儲的屬性。
其他預備知識:
每個cookie在實際存儲的時候,會有下述字段:name, value, expiry-time, domain, path, creation-time, last-access-time, persistent-flag, host-only-flag, secure-only-flag, and http-only-flag??梢钥闯鲞@些字段和設置cookie時的屬性并不是一對一關系,其中,
- expiry-time是通過Expires和Max-Age得出來的;
- persistent-flag和是否設置了過期時間有關;
- creation-time(創建時間)和last-access-time(最后一次訪問時間)是瀏覽器自動存儲的屬性。
document.cookie賦值語法
這部分內容主要是基于以下問題:
document.cookie = ''
能否用來清除cookie?
首先看下document.cookie的定義:
TheDocument
propertycookie
lets you read and write cookies associated with the document. It serves as a getter and setter for the actual values of the cookies.
從上面可以看到,document.cookie只是提供了一個getter和setter方法來訪問和設置cookie,并不是直接修改cookie的值。什么意思呢?
我們知道,JS提供了兩種類型的屬性,data property和accessor property。分別舉個例子對比看下:
// data property
var obj1 = {
hello: 'name'
}
Object.getOwnPropertyDescriptor(obj1, 'hello')
// accessor property
var realHello = '' // 實際存儲obj2.hello的變量
var obj2 = {
get hello () {
return 'custom get ' + realHello
},
set hello (val) {
realHello = 'custom set ' + val
}
}
obj2.hello = 'name'
console.log(obj2.hello)
Object.getOwnPropertyDescriptor(obj2, 'hello')
可以看到用于描述obj1
的hello
屬性的是value和writable,用于描述obj2
的hello
屬性的是get
和set
。簡單的說,獲取和修改data property
是直接的,獲取和修改accessor property
是間接的,可以通過該屬性的get
和set
做自定義處理。我們熟悉的Vue 2.0響應式的data的屬性就是accessor property。
document的cookie屬性也是accessor property。驗證的時候發現一個有意思的問題,最終是在document的原型上找到的屬性描述符,以后有時間了研究下:
Object.getOwnPropertyDescriptor(document, 'cookie') // undefined
Object.getOwnPropertyDescriptor(document.__proto__.__proto__, 'cookie')
我們看下document.cookie的語法:
document.cookie = newCookie;
修改的時候,newCookie的格式需要滿足如下形式,且一次只能設置/更新單個cookie:
In the code above,newCookie
is a string of formkey=value
. Note that you can only set/update a single cookie at a time using this method.
舉個例子,打開MDN document.cookie頁面。添加一個cookie,每個cookie后面可以添加和該cookie相關的一系列屬性,通過分號;
分割:
// cookie默認過期時間是session,也就是瀏覽器關閉的時候,該cookie就失效了
document.cookie = 'hello1=world1;'
// 使用max-age設置過期時間:max-age以秒為單位,設置1個小時之后過期:3600 = 60 * 60
document.cookie = 'hello2=world2;max-age=3600'
// 使用expires設置過期時間:expires是GMT形式的日期格式,設置1小時之后過期
var current = new Date()
current.setTime(current.getTime() + 3600000) // setTime單位是ms,3600000 = 60 * 60 * 1000
document.cookie = `hello3=world3;expires=${current.toUTCString()}`
Chrome Application Cookies內容截圖如下:
既然document.cookie只能設置/更新單個cookie,當賦值為空字符串的時候,那就是什么都沒有做。也就是document.cookie = ''
不會產生任何作用,不能用于清除cookie。
那么,
問:如何使用document.cookie清除一個cookie呢?
答:可以在設置cookie的時候,設置一個已經過去的過期時間:
// 使用expires設置一個過去一小時之前的時間
document.cookie = 'hello2=world2;max-age=-3600'
// 使用expires設置一個過去一小時之前的時間
var current = new Date()
current.setTime(current.getTime() - 3600000)
document.cookie = `hello3=world3;expires=${current.toUTCString()}`
現在,只剩下了一個hello1:
注:瀏覽器開發者工具是提供了清除cookie的操作的,上述主要討論的是如何通過js清除cookie。
cookie賦值算法
這部分內容主要是基于以下兩個問題:
- 同時設置了expires和max-age,哪個有效?
- 假設已經有一個名為hello的cookie,再次設置名為hello的cookie的時候是新增還是修改?
問題1: 同時設置了expires和max-age,哪個有效?
前面的例子中,我使用了expires和max-age,我的疑問是感覺這兩個屬性是干了一樣的事情,如果我同時設置了這兩個屬性的話,哪個生效呢?
帶著這個疑問,找到了RFC 6265 - HTTP State Management Mechanism。這個規范里面明確定義了如何設置/更新cookie。
就下面的例子,我們按照文檔找一下答案:
var current = new Date()
console.log('now: ', current.toUTCString()) // now: Sat, 20 Feb 2021 11:41:02 GMT
current.setTime(current.getTime() + 3600000)
// 使用expires設置為一小時之后過期:60 * 60 * 1000 = 3600000ms
// 使用max-age設置為兩小時之后過期:2 * 60 * 60 = 7200s
document.cookie = `hello=world;expires=${current.toUTCString()};max-age=7200` // hello=world;expires=Sat, 20 Feb 2021 12:41:02 GMT;max-age=7200
這部分的內容主要是在第五部分的5.2。首先,它會把設置的這個值通過逗號分割成多個部分,然后通過等號把每個部分分成key-value的形式:
hello=world;expires=Sat, 20 Feb 2021 12:41:02 GMT;max-age=7200
// 變為
hello=world // 第一個分號前面的是這個cookie的name和value,后面的是這個cookie的屬性
expires=Sat, 20 Feb 2021 12:41:02 GMT // 屬性
max-age=7200 // 屬性
// 變為
hello: world
expires: expires=Sat, 20 Feb 2021 12:41:02 GMT
max-age: 7200
然后看每個屬性,文檔中5.2.*部分,算法會解析每個key-value,并放在一個cookie屬性列表cookie-attribute-list里面,分析屬性的時候不區分大小寫,expires和max-age轉化為:
Expires: expires=Sat, 20 Feb 2021 12:41:02 GMT
Max-Age: expires=Sat, 20 Feb 2021 13:41:02 GMT
屬性都分析完之后,我們可以得到和每條cookie相關的三個部分:cookie-name(cookie名),cookie-value(cookie值),cookie-attribute-list(cookie屬性列表)。然后進入設置階段,關鍵在5.3部分的第三步:
首先看上面生成的cookie屬性列表里面是否包含Max-Age,如果包含,把cookie的過期時間設置成最后一個Max-Age的值;如果沒有Max-Age,才會查看是否包含Expires,如果包含,把cookie的過期時間設置成最后一個Expires的值。這里last attribute的意思是我們在給cookie賦值的時候,是可以寫多個max-age的,這個時候取最后一個語法有效的max-age。
所以,簡單的說:當expires和max-age同時出現的時候,max-age的優先級更高。
Chrome截圖也證明了max-age優先級更高:
假設已經有一個名為hello的cookie,再次設置名為hello的cookie是新增還是修改?
我們接著往下看5.3部分的第11、12步:
在11步中,首先判斷已有cookie中是否有和本cookie同時具有相同的name(名稱),domain(域),path(路徑)的cookie。
11.1 如果有,標記為old-cookie
;
11.2 如果新cookie不是通過http的方式設置的,并且old-cookie設置了只能用于http,忽略新cookie;
11.3 把新cookie的creation-time(創建時間)字段更新為old-cookie的創建時間;
11.4 從cookie表中移除old-cookie;
12 把新cookie插入到cookie表里面。
所以,可以得出:判斷是新增還是修改的標準是cookie表中是否存在和新cookie的name,domain和path屬性都一樣的cookie。
在設置cookie的時候,如果設置結果和你想要的不符,可以按照上面例子的思路,查看RFC 6265 - HTTP State Management Mechanism文檔的5.2和5.3部分。
cookie取值算法
這部分內容主要是基于以下兩個問題:
- 如何把字符串形式的cookie改成json的形式?
- 如果設置了多個同名cookie,獲取cookie時以哪個為準?
如何把字符串形式的cookie改成json的形式?
之所以會遇到這個問題,主要是解決下面這個場景:
如果通過WebSocket發送請求,在連接建立的時候,是可以獲取cookie信息的。但是,一旦連接建立成功,WebSocket一直連接的時候,發送的數據是不會攜帶cookie信息的。如果在連接建立之后,用戶做了登出操作,這個時候其實不應該再響應用戶請求,并且斷開連接。
這時,問題來了,如何判斷當用戶登出的時候不再響應用戶請求呢?
其實和http一樣,每次請求帶上cookie信息就可以了。只不過發送http請求的時候,瀏覽器幫我們做了這部分工作。WebSocket的話,就得我們手動添加上了。
我們可以通過document.cookie獲取cookie值,但是該值是一個表示所有cookie的字符串。為了獲取某個cookie的值,我們就得自己解析。要想解析,就得知道cookie字符串的取值規則。這部分內容主要是RFC 6265 - HTTP State Management Mechanism文檔的5.4部分。
- 通過步驟4的第2步,可以知道不同的cookie是通過分號和空格
;
拼接起來的; - 通過步驟4的第1步,可以知道最終結果中只會出現cookie的name和value,cookie的屬性并不會出現。name和value是通過等號
=
拼接起來的;
所以,解析cookie的為代碼可以表示如下:
var cookieStr = document.cookie
var cookieArray = cookieStr.split('; ') // 獲得每個cookie的字符串形式組成的數組
var cookieObj = {}
cookieArray.forEach(item => {
var index = item.indexOf('=') // 獲取每個cookie字符串的key和value
if (index !== -1) { // 沒有使用split的原因是value里面也是可以包含=號的
cookieObj[item.slice(0, index)] = item.slice(index + 1)
}
})
console.log(cookieObj)
如果設置了多個同名cookie,獲取cookie時以哪個為準?
例如,有下面幾個cookie:
document.cookie = 'hello4=world4;'
document.cookie = 'hello5=world;'
document.cookie = 'hello5=worldShortPath;Path=/en-US'
document.cookie = 'hello6=world6;'
Chrome截圖如下:
這部分內容還是在RFC 6265 - HTTP State Management Mechanism文檔的[5.4部分],只不過是在第2步。首先第1步是從cookie store(cookie列表)中找到和請求地址相關的所有cookie。然后執行第2步:
- 通過步驟2的第1步,可以知道具有更長path屬性的cookie是放在前面的;
- 通過步驟2的第2步,可以知道path屬性長度相同的時候,creation-times創建時間越早的放在前面。
綜上,所有同名的cookie都會出現在最終的cookie字符串中。并且,cookie是先按照path屬性就行排序,然后按照creation-times創建時間屬性進行排序。但是,步驟二下面有注釋,這種排序方式是實踐得出的比較好的排序方式,但是瀏覽器不一定按照這種排序方式實現。
就上面的例子,我們在設置完cookie之后,在控制臺中查看document.cookie:
console.log(document.cookie) // hello4=world4; hello5=world; hello6=world6; hello5=worldShortPath
可以看到hello5的路徑最短,所以放在最后,其他的按照設置順序,也就是創建時間排序。
總結
在cookie賦值語法部分,我們知道document.cookie = ''
不能用來清除cookie;
在cookie賦值算法部分,我們知道:
- 同時設置了expires和max-age,max-age有效;
- 假設已經有一個名為hello的cookie,再次設置名為hello的cookie的時候,如果name,domain和path屬性完全相同就是修改,否則是新增;
在cookie取值算法部分,我們知道:
- 如何把字符串形式的cookie改成json的形式的時候,可以先通過
;
分割一次,在通過=
分割一次; - 如果設置了多個同名cookie,所有同名的cookie都會出現在最終的cookie字符串中,按照path屬性的長度和creation-times排序。
如果你遇到和cookie賦值語法、賦值算法和取值算法相關的問題,可以參照上述部分遇到的問題的解決思路去看看能否解決問題。
如有錯誤,歡迎留言討論。