一、Cookie 在前端中的實(shí)踐
1.搭建Demo環(huán)境
找個(gè)文件夾,npm init,然后如果沒有安裝過express,再npm install express -D,然后新建一個(gè)main.js,執(zhí)行node main.js即可啟動(dòng)服務(wù)。注意如果修改了js腳本,需要重新執(zhí)行node main.js。
const express = require('express')
const app = express()
app.listen(3000, err => {
if (err) {
return console.log(err)
}
console.log('---- 打開 http://localhost:3000 吧----')
})
app.get('/', (req, res) => {
res.send('<h1>hello world!</h1>')
})
2.在介紹 Cookie 是什么之前,我們來看看 Cookie 是如何工作的
- 首先,我們假設(shè)當(dāng)前域名下還是沒有 Cookie 的
- 接下來,瀏覽器發(fā)送了一個(gè)請(qǐng)求給服務(wù)器(這個(gè)請(qǐng)求是還沒帶上 Cookie 的)
- 服務(wù)器設(shè)置 Cookie 并發(fā)送給瀏覽器(當(dāng)然也可以不設(shè)置)
- 瀏覽器將 Cookie 保存下來
- 接下來,以后的每一次請(qǐng)求,都會(huì)帶上這些 Cookie,發(fā)送給服務(wù)器
app.get('/', (req, res) => {
// 服務(wù)器接收到請(qǐng)求,在給響應(yīng)設(shè)置一個(gè) Cookie
// 這個(gè) Cookie 的 name 為 testName
// value 為 testValue
res.cookie('testName', 'testValue')
res.send('<h1>hello world!</h1>')
})
改一下代碼,就可以驗(yàn)證以上邏輯了,第一次Request Headers 并沒有 Cookie 這個(gè)字段,后面刷新就一直有了。當(dāng)然,如果我們多設(shè)置幾個(gè)cookie,請(qǐng)求時(shí)也會(huì)匯總帶上。
app.get('/', (req, res) => {
res.cookie('testName', 'testValue')
res.cookie('testName22', 'testValue')
res.cookie('testName33', 'testValue')
res.cookie('myName', 'cuixu')
res.send('<h1>hello world!</h1>')
})


3.什么是 Cookie
說了這么多,大家應(yīng)該知道 Cookie 是什么吧。整理一下有以下幾個(gè)點(diǎn):
- Cookie 就是瀏覽器儲(chǔ)存在用戶電腦上的一小段文本文件
- Cookie 是純文本格式,不包含任何可執(zhí)行的代碼
- Cookie 由鍵值對(duì)構(gòu)成,由分號(hào)和空格隔開
- Cookie 雖然是存儲(chǔ)在瀏覽器,但是通常由服務(wù)器端進(jìn)行設(shè)置
- Cookie 的大小限制在 4kb 左右
4.Cookie 的屬性選項(xiàng)
expires / max-age 都是控制 Cookie 失效時(shí)刻的選項(xiàng)。
// 這個(gè) Cookie 設(shè)置十秒后失效
res.cookie('testName0', 'testValue0', {
expires: new Date(Date.now() + 10000)
})
在控制臺(tái)輸入下面的代碼
console.log(`現(xiàn)在的 cookie 是:${document.cookie}`)
setTimeout(() => {
console.log(`5 秒后的 cookie 是:${document.cookie}`)
}, 5000)
setTimeout(() => {
console.log(`10 秒后的 cookie 是:${document.cookie}`)
}, 10000)
可以發(fā)現(xiàn),10秒失效后,自動(dòng)就沒了,當(dāng)然以后發(fā)送請(qǐng)求也不會(huì)再帶上這個(gè)失效的 Cookie 了。
expires 是 http/1.0 協(xié)議中的選項(xiàng),在新的 http/1.1 協(xié)議中 expires 已經(jīng)由 max-age 選項(xiàng)代替,兩者的作用都是限制 Cookie 的有效時(shí)間。expires 的值是一個(gè)時(shí)間點(diǎn) (Cookie 失效時(shí)刻 = expires),而 max-age 的值是一個(gè)以秒為單位時(shí)間段 (Cookie 失效時(shí)刻 = 創(chuàng)建時(shí)刻 + max-age)。如果同時(shí)設(shè)置了 max-age 和 expires,以 max-age 的時(shí)間為準(zhǔn)。
res.cookie('testName0', 'testValue0', {
// express 這個(gè)參數(shù)是以毫秒來做單位的
// 實(shí)際發(fā)送給瀏覽器就會(huì)轉(zhuǎn)換為秒
// 十秒后失效
maxAge: 10000
})
name、domain 和 path 可以標(biāo)識(shí)一個(gè)唯一的 Cookie。domain 和 path 兩個(gè)選項(xiàng)共同決定了 Cookie 何時(shí)被瀏覽器自動(dòng)添加到請(qǐng)求頭部中發(fā)送出去。具體是什么原理請(qǐng)看 Cookie 的作用域和作用路徑 這個(gè)章節(jié)。如果沒有設(shè)置這兩個(gè)選項(xiàng),則會(huì)使用默認(rèn)值。domain 的默認(rèn)值為設(shè)置該 Cookie 的網(wǎng)頁所在的域名,path 默認(rèn)值為設(shè)置該 Cookie 的網(wǎng)頁所在的目錄。
secure 選項(xiàng)用來設(shè)置 Cookie 只在確保安全的請(qǐng)求中才會(huì)發(fā)送。當(dāng)請(qǐng)求是 HTTPS 或者其他安全協(xié)議時(shí),包含 secure 選項(xiàng)的 Cookie 才能被保存到瀏覽器或者發(fā)送至服務(wù)器。默認(rèn)情況下,Cookie 不會(huì)帶 secure 選項(xiàng)(即為空)。所以默認(rèn)情況下,不管是 HTTPS 協(xié)議還是 HTTP 協(xié)議的請(qǐng)求,Cookie 都會(huì)被發(fā)送至服務(wù)端。
httpOnly這個(gè)選項(xiàng)用來設(shè)置 Cookie 是否能通過 js 去訪問。默認(rèn)情況下,Cookie 不會(huì)帶 httpOnly 選項(xiàng)(即為空),客戶端是可以通過 js 代碼去訪問(包括讀取、修改、刪除等)這個(gè) Cookie 的。當(dāng) Cookie 帶 httpOnly 選項(xiàng)時(shí),客戶端則無法通過 js 代碼去訪問(包括讀取、修改、刪除等)這個(gè) Cookie。
5.設(shè)置 Cookie
明確一點(diǎn):Cookie 可以由服務(wù)端設(shè)置,也可以由客戶端設(shè)置??吹竭@里相信大家都可以理解了吧。在網(wǎng)頁即客戶端中我們也可以通過 js 代碼來設(shè)置 Cookie。
- 設(shè)置
document.cookie = 'name=value' - 可以設(shè)置 Cookie 的下列選項(xiàng):expires、domain、path,各個(gè)鍵值對(duì)之間都要用 ; 和 空格 隔開:
document.cookie='name=value; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/'; - 只有在 https 協(xié)議的網(wǎng)頁中,客戶端設(shè)置 secure 類型的 Cookie 才能成功
- 客戶端中無法設(shè)置 HttpOnly 選項(xiàng)
- Cookie 的 name、path 和 domain 是唯一標(biāo)識(shí)一個(gè) Cookie 的。我們只要將一個(gè) Cookie 的 max-age 設(shè)置為 0,就可以刪除一個(gè) Cookie 了。
let removeCookie = (name, path, domain) => {
document.cookie = `${name}=; path=${path}; domain=${domain}; max-age=0`
}
6.Cookie 的作用域
在說這個(gè)作用域之前,我們先來對(duì)域名做一個(gè)簡(jiǎn)單的了解。
子域,是相對(duì)父域來說的,指域名中的每一個(gè)段。各子域之間用小數(shù)點(diǎn)分隔開。放在域名最后的子域稱為最高級(jí)子域,或稱為一級(jí)域,在它前面的子域稱為二級(jí)域。
以下圖為例,news.163.com 和 sports.163.com 是子域,163.com 是父域。當(dāng) Cookie 的 domain 為 news.163.com,那么訪問 news.163.com 的時(shí)候就會(huì)帶上 Cookie;當(dāng) Cookie 的 domain 為 163.com,那么訪問 news.163.com 和 sports.163.com 就會(huì)帶上 Cookie
7.Cookie 的作用路徑
當(dāng) Cookie 的 domain 是相同的情況下,也有是否帶上 Cookie 也有一定的規(guī)則。在子路徑內(nèi)可以訪問訪問到父路徑的 Cookie,反過來就不行。
看看例子,還是先修改 main.js
app.get('/parent', (req, res) => {
res.cookie('parent-name', 'parent-value', {
path: '/parent'
})
res.send('<h1>父路徑!</h1>')
})
app.get('/parent/childA', (req, res) => {
res.cookie('child-name-A', 'child-value-A', {
path: '/parent/childA'
})
res.send('<h1>子路徑A!</h1>')
})
app.get('/parent/childB', (req, res) => {
res.cookie('child-name-B', 'child-value-B', {
path: '/parent/childB'
})
res.send('<h1>子路徑B!</h1>')
})
參考文章
二、一文帶你看懂cookie,面試前端不用愁
1.存放哪些數(shù)據(jù)
當(dāng)客戶端要發(fā)送http請(qǐng)求時(shí),瀏覽器會(huì)先檢查下是否有對(duì)應(yīng)的cookie。有的話,則自動(dòng)地添加在request header中的cookie字段。注意,每一次的http請(qǐng)求時(shí),如果有cookie,瀏覽器都會(huì)自動(dòng)帶上cookie發(fā)送給服務(wù)端。那么把什么數(shù)據(jù)放到cookie中就很重要了,因?yàn)楹芏鄶?shù)據(jù)并不是每次請(qǐng)求都需要發(fā)給服務(wù)端,畢竟會(huì)增加網(wǎng)絡(luò)開銷,浪費(fèi)帶寬。所以對(duì)于那設(shè)置“每次請(qǐng)求都要攜帶的信息(最典型的就是身份認(rèn)證信息)”就特別適合放在cookie中,其他類型的數(shù)據(jù)就不適合了。
2.localStorage和sessionStorage
在較高版本的瀏覽器中,js提供了兩種存儲(chǔ)方式:sessionStorage和globalStorage。在H5中,用localStorage取代了globalStorage。
sessionStorage用于本地存儲(chǔ)一個(gè)會(huì)話中的數(shù)據(jù),這些數(shù)據(jù)只有在同一個(gè)會(huì)話中的頁面才能訪問,并且當(dāng)會(huì)話結(jié)束后,數(shù)據(jù)也隨之銷毀。所以sessionStorage僅僅是會(huì)話級(jí)別的存儲(chǔ),而不是一種持久化的本地存儲(chǔ)。
localStorage是持久化的本地存儲(chǔ),除非是通過js刪除,或者清除瀏覽器緩存,否則數(shù)據(jù)是永遠(yuǎn)不會(huì)過期的。
瀏覽器的支持情況:IE7及以下版本不支持web storage,其他都支持。不過在IE5、IE6、IE7中有個(gè)userData,其實(shí)也是用于本地存儲(chǔ)。這個(gè)持久化數(shù)據(jù)放在緩存中,只有不清理緩存,就會(huì)一直存在。
三、知乎 COOKIE和SESSION有什么區(qū)別?
由于HTTP協(xié)議是無狀態(tài)的協(xié)議,所以服務(wù)端需要記錄用戶的狀態(tài)時(shí),就需要用某種機(jī)制來識(shí)具體的用戶,這個(gè)機(jī)制就是Session.典型的場(chǎng)景比如購(gòu)物車,當(dāng)你點(diǎn)擊下單按鈕時(shí),由于HTTP協(xié)議無狀態(tài),所以并不知道是哪個(gè)用戶操作的,所以服務(wù)端要為特定的用戶創(chuàng)建了特定的Session,用用于標(biāo)識(shí)這個(gè)用戶,并且跟蹤用戶,這樣才知道購(gòu)物車?yán)锩嬗袔妆緯_@個(gè)Session是保存在服務(wù)端的,有一個(gè)唯一標(biāo)識(shí)。在服務(wù)端保存Session的方法很多,內(nèi)存、數(shù)據(jù)庫、文件都有。集群的時(shí)候也要考慮Session的轉(zhuǎn)移,在大型的網(wǎng)站,一般會(huì)有專門的Session服務(wù)器集群,用來保存用戶會(huì)話,這個(gè)時(shí)候 Session 信息都是放在內(nèi)存的,使用一些緩存服務(wù)比如Memcached之類的來放 Session。
思考一下服務(wù)端如何識(shí)別特定的客戶?這個(gè)時(shí)候Cookie就登場(chǎng)了。每次HTTP請(qǐng)求的時(shí)候,客戶端都會(huì)發(fā)送相應(yīng)的Cookie信息到服務(wù)端。實(shí)際上大多數(shù)的應(yīng)用都是用 Cookie 來實(shí)現(xiàn)Session跟蹤的,第一次創(chuàng)建Session的時(shí)候,服務(wù)端會(huì)在HTTP協(xié)議中告訴客戶端,需要在 Cookie 里面記錄一個(gè)Session ID,以后每次請(qǐng)求把這個(gè)會(huì)話ID發(fā)送到服務(wù)器,我就知道你是誰了。有人問,如果客戶端的瀏覽器禁用了 Cookie 怎么辦?一般這種情況下,會(huì)使用一種叫做URL重寫的技術(shù)來進(jìn)行會(huì)話跟蹤,即每次HTTP交互,URL后面都會(huì)被附加上一個(gè)諸如 sid=xxxxx 這樣的參數(shù),服務(wù)端據(jù)此來識(shí)別用戶。
Cookie其實(shí)還可以用在一些方便用戶的場(chǎng)景下,設(shè)想你某次登陸過一個(gè)網(wǎng)站,下次登錄的時(shí)候不想再次輸入賬號(hào)了,怎么辦?這個(gè)信息可以寫到Cookie里面,訪問網(wǎng)站的時(shí)候,網(wǎng)站頁面的腳本可以讀取這個(gè)信息,就自動(dòng)幫你把用戶名給填了,能夠方便一下用戶。這也是Cookie名稱的由來,給用戶的一點(diǎn)甜頭。
所以,總結(jié)一下:Session是在服務(wù)端保存的一個(gè)數(shù)據(jù)結(jié)構(gòu),用來跟蹤用戶的狀態(tài),這個(gè)數(shù)據(jù)可以保存在集群、數(shù)據(jù)庫、文件中;Cookie是客戶端保存用戶信息的一種機(jī)制,用來記錄用戶的一些信息,也是實(shí)現(xiàn)Session的一種方式。
四、徹底理解cookie,session,token
1.發(fā)展史
很久很久以前,Web 基本上就是文檔的瀏覽而已, 既然是瀏覽,作為服務(wù)器, 不需要記錄誰在某一段時(shí)間里都瀏覽了什么文檔,每次請(qǐng)求都是一個(gè)新的HTTP協(xié)議, 就是請(qǐng)求加響應(yīng), 尤其是我不用記住是誰剛剛發(fā)了HTTP請(qǐng)求, 每個(gè)請(qǐng)求對(duì)我來說都是全新的。這段時(shí)間很嗨皮。
但是隨著交互式Web應(yīng)用的興起,像在線購(gòu)物網(wǎng)站,需要登錄的網(wǎng)站等等,馬上就面臨一個(gè)問題,那就是要管理會(huì)話,必須記住哪些人登錄系統(tǒng), 哪些人往自己的購(gòu)物車中放商品, 也就是說我必須把每個(gè)人區(qū)分開,這就是一個(gè)不小的挑戰(zhàn),因?yàn)镠TTP請(qǐng)求是無狀態(tài)的,所以想出的辦法就是給大家發(fā)一個(gè)會(huì)話標(biāo)識(shí)(session id), 說白了就是一個(gè)隨機(jī)的字串,每個(gè)人收到的都不一樣, 每次大家向我發(fā)起HTTP請(qǐng)求的時(shí)候,把這個(gè)字符串給一并捎過來, 這樣我就能區(qū)分開誰是誰了
這樣大家很嗨皮了,可是服務(wù)器就不嗨皮了,每個(gè)人只需要保存自己的session id,而服務(wù)器要保存所有人的session id ! 如果訪問服務(wù)器多了, 就得由成千上萬,甚至幾十萬個(gè)。
這對(duì)服務(wù)器說是一個(gè)巨大的開銷 , 嚴(yán)重的限制了服務(wù)器擴(kuò)展能力, 比如說我用兩個(gè)機(jī)器組成了一個(gè)集群, 小F通過機(jī)器A登錄了系統(tǒng), 那session id會(huì)保存在機(jī)器A上, 假設(shè)小F的下一次請(qǐng)求被轉(zhuǎn)發(fā)到機(jī)器B怎么辦? 機(jī)器B可沒有小F的 session id啊。
有時(shí)候會(huì)采用一點(diǎn)小伎倆: session sticky , 就是讓小F的請(qǐng)求一直粘連在機(jī)器A上, 但是這也不管用, 要是機(jī)器A掛掉了, 還得轉(zhuǎn)到機(jī)器B去。
那只好做session 的復(fù)制了, 把session id 在兩個(gè)機(jī)器之間搬來搬去, 快累死了。
后來有個(gè)叫Memcached的支了招: 把session id 集中存儲(chǔ)到一個(gè)地方, 所有的機(jī)器都來訪問這個(gè)地方的數(shù)據(jù), 這樣一來,就不用復(fù)制了, 但是增加了單點(diǎn)失敗的可能性, 要是那個(gè)負(fù)責(zé)session 的機(jī)器掛了, 所有人都得重新登錄一遍, 估計(jì)得被人罵死。
也嘗試把這個(gè)單點(diǎn)的機(jī)器也搞出集群,增加可靠性, 但不管如何, 這小小的session 對(duì)我來說是一個(gè)沉重的負(fù)擔(dān)
于是有人就一直在思考, 我為什么要保存這可惡的session呢, 只讓每個(gè)客戶端去保存該多好?
可是如果不保存這些session id , 怎么驗(yàn)證客戶端發(fā)給我的session id 的確是我生成的呢? 如果不去驗(yàn)證,我們都不知道他們是不是合法登錄的用戶, 那些不懷好意的家伙們就可以偽造session id , 為所欲為了。
嗯,對(duì)了,關(guān)鍵點(diǎn)就是驗(yàn)證 !
比如說, 小F已經(jīng)登錄了系統(tǒng), 我給他發(fā)一個(gè)令牌(token), 里邊包含了小F的 user id, 下一次小F 再次通過Http 請(qǐng)求訪問我的時(shí)候, 把這個(gè)token 通過Http header 帶過來不就可以了。
不過這和session id沒有本質(zhì)區(qū)別啊, 任何人都可以可以偽造, 所以我得想點(diǎn)兒辦法, 讓別人偽造不了。
那就對(duì)數(shù)據(jù)做一個(gè)簽名吧, 比如說我用HMAC-SHA256 算法,加上一個(gè)只有我才知道的密鑰, 對(duì)數(shù)據(jù)做一個(gè)簽名, 把這個(gè)簽名和數(shù)據(jù)一起作為token , 由于密鑰別人不知道, 就無法偽造token了。

這個(gè)token 我不保存, 當(dāng)小F把這個(gè)token 給我發(fā)過來的時(shí)候,我再用同樣的HMAC-SHA256 算法和同樣的密鑰,對(duì)數(shù)據(jù)再計(jì)算一次簽名, 和token 中的簽名做個(gè)比較, 如果相同, 我就知道小F已經(jīng)登錄過了,并且可以直接取到小F的user id , 如果不相同, 數(shù)據(jù)部分肯定被人篡改過, 我就告訴發(fā)送者: 對(duì)不起,沒有認(rèn)證。

Token 中的數(shù)據(jù)是明文保存的(雖然我會(huì)用Base64做下編碼, 但那不是加密), 還是可以被別人看到的, 所以我不能在其中保存像密碼這樣的敏感信息。
當(dāng)然, 如果一個(gè)人的token 被別人偷走了, 那我也沒辦法, 我也會(huì)認(rèn)為小偷就是合法用戶, 這其實(shí)和一個(gè)人的session id 被別人偷走是一樣的。
這樣一來, 我就不保存session id 了, 我只是生成token , 然后驗(yàn)證token , 我用我的CPU計(jì)算時(shí)間獲取了我的session存儲(chǔ)空間 !
解除了session id這個(gè)負(fù)擔(dān), 可以說是無事一身輕, 我的機(jī)器集群現(xiàn)在可以輕松地做水平擴(kuò)展, 用戶訪問量增大, 直接加機(jī)器就行。 這種無狀態(tài)的感覺實(shí)在是太好了!
以下參考前端應(yīng)該知道的web登錄
前面說到sessionId的方式本質(zhì)是把用戶狀態(tài)信息維護(hù)在server端,token的方式就是把用戶的狀態(tài)信息加密成一串token傳給前端,然后每次發(fā)請(qǐng)求時(shí)把token帶上,傳回給服務(wù)器端;服務(wù)器端收到請(qǐng)求之后,解析token并且驗(yàn)證相關(guān)信息;
所以跟第一種登錄方式最本質(zhì)的區(qū)別是:通過解析token的計(jì)算時(shí)間換取了session的存儲(chǔ)空間
業(yè)界通用的加密方式是jwt(json web token),jwt的具體格式如圖:

簡(jiǎn)單的介紹一下jwt,它主要由3部分組成:
header 頭部
{
"alg": "HS256",
"typ": "JWT"
}
payload 負(fù)載
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1555341649998
}
signature 簽名
header里面描述加密算法和token的類型,類型一般都是JWT;
payload里面放的是用戶的信息,也就是第一種登錄方式中需要維護(hù)在服務(wù)器端session中的信息;
signature是對(duì)前兩部分的簽名,也可以理解為加密;實(shí)現(xiàn)需要一個(gè)密鑰(secret),這個(gè)secret只有服務(wù)器才知道,然后使用header里面的算法按照如下方法來加密:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
總之,最后的 jwt = base64url(header) + "." + base64url(payload) + "." + signature
jwt可以放在response中返回,也可以放在cookie中返回,這都是具體的返回方式,并不重要。
客戶端發(fā)起請(qǐng)求時(shí),官方推薦放在HTTP header中:
Authorization: Bearer <token>
這樣子確實(shí)也可以解決cookie跨域的問題,不過具體放在哪兒還是根據(jù)業(yè)務(wù)場(chǎng)景來定,并沒有一定之規(guī)。
五、Web技術(shù):Token與Session究竟是什么呢
本文通俗易懂,基本是上述內(nèi)容的重述。文末總結(jié)有點(diǎn)意思:
wa: 先不提問,我再說幾個(gè)結(jié)論,在提問。。
- token 是無狀態(tài)的,后端不需要記錄信息,每次請(qǐng)求每次解密就行。
- session 是有狀態(tài)的,需要后端每次去檢索id的有效性。不同的session都需要進(jìn)行保存哦。但讓也可以設(shè)置單點(diǎn)登錄,減少保存的數(shù)據(jù)。
- session與token的問題是空間與時(shí)間博弈,為什么這么說呢,是因?yàn)閠oken不需要保存,直接獲取,每次訪問都需要進(jìn)行解密。
好了就先說這幾條吧,以后有了在補(bǔ)充。
琪琪:總結(jié)了這么多了,那我們開始提問了。首先,為啥客戶端ios與Andriod基本上沒見過用session的?
ff: 這個(gè)我來回答吧。因?yàn)樵谶@些客戶端上啊,原生接口都是每一次建立一個(gè)會(huì)話,這就出問題了,這樣會(huì)導(dǎo)致登錄功能失效了,登錄每次把信息放到session中,session都不一樣了,每次登錄都成新的一個(gè)人了,這就不ok了。
琪琪:哦哦原來客戶端是每次用原生接口都是新建立一個(gè)會(huì)話,好尷尬的設(shè)計(jì)。那我們采用什么方式解決這個(gè)問題呢?
ff: 在用戶登錄后,重點(diǎn)哦我們可以通過cookie嘛,我們?cè)赼pp端也可以是存儲(chǔ)cookie的,我們知道cookie將sessionID保存好,返回給客戶端,服務(wù)器最后也是通過SessionId來標(biāo)識(shí)的。但是利用token的話,內(nèi)容我們可以自定義,并且不用再服務(wù)端進(jìn)行保存。方便我們處理。
琪琪:那么就是token可以保存到cookie中,如果禁止的話我們也可以保存到body中每次都請(qǐng)求上或者h(yuǎn)eader中。