鍍金池/ 教程/ HTML/ cookie 和 session
瀏覽器端測(cè)試:mocha,chai,phantomjs
搭建 Node.js 開(kāi)發(fā)環(huán)境
測(cè)試用例:mocha,should,istanbul
線(xiàn)上部署:heroku
Mongodb 與 Mongoose 的使用
使用 superagent 與 cheerio 完成簡(jiǎn)單爬蟲(chóng)
js 中的那些最佳實(shí)踐
使用 eventproxy 控制并發(fā)
使用 promise 替代回調(diào)函數(shù)
作用域與閉包:this,var,(function () {})
持續(xù)集成平臺(tái):travis
測(cè)試用例:supertest
benchmark 怎么寫(xiě)
使用 async 控制并發(fā)
學(xué)習(xí)使用外部模塊
一個(gè)最簡(jiǎn)單的 express 應(yīng)用
正則表達(dá)式
cookie 和 session

cookie 和 session

眾所周知,HTTP 是一個(gè)無(wú)狀態(tài)協(xié)議,所以客戶(hù)端每次發(fā)出請(qǐng)求時(shí),下一次請(qǐng)求無(wú)法得知上一次請(qǐng)求所包含的狀態(tài)數(shù)據(jù),如何能把一個(gè)用戶(hù)的狀態(tài)數(shù)據(jù)關(guān)聯(lián)起來(lái)呢?

比如在淘寶的某個(gè)頁(yè)面中,你進(jìn)行了登陸操作。當(dāng)你跳轉(zhuǎn)到商品頁(yè)時(shí),服務(wù)端如何知道你是已經(jīng)登陸的狀態(tài)?

cookie

首先產(chǎn)生了 cookie 這門(mén)技術(shù)來(lái)解決這個(gè)問(wèn)題,cookie 是 http 協(xié)議的一部分,它的處理分為如下幾步:

  • 服務(wù)器向客戶(hù)端發(fā)送 cookie。
    • 通常使用 HTTP 協(xié)議規(guī)定的 set-cookie 頭操作。
    • 規(guī)范規(guī)定 cookie 的格式為 name = value 格式,且必須包含這部分。
  • 瀏覽器將 cookie 保存。
  • 每次請(qǐng)求瀏覽器都會(huì)將 cookie 發(fā)向服務(wù)器。

其他可選的 cookie 參數(shù)會(huì)影響將 cookie 發(fā)送給服務(wù)器端的過(guò)程,主要有以下幾種:

  • path:表示 cookie 影響到的路徑,匹配該路徑才發(fā)送這個(gè) cookie。
  • expires 和 maxAge:告訴瀏覽器這個(gè) cookie 什么時(shí)候過(guò)期,expires 是 UTC 格式時(shí)間,maxAge 是 cookie 多久后過(guò)期的相對(duì)時(shí)間。當(dāng)不設(shè)置這兩個(gè)選項(xiàng)時(shí),會(huì)產(chǎn)生 session cookie,session cookie 是 transient 的,當(dāng)用戶(hù)關(guān)閉瀏覽器時(shí),就被清除。一般用來(lái)保存 session 的 session_id。
  • secure:當(dāng) secure 值為 true 時(shí),cookie 在 HTTP 中是無(wú)效,在 HTTPS 中才有效。
  • httpOnly:瀏覽器不允許腳本操作 document.cookie 去更改 cookie。一般情況下都應(yīng)該設(shè)置這個(gè)為 true,這樣可以避免被 xss 攻擊拿到 cookie。

express 中的 cookie

express 在 4.x 版本之后,session管理和cookies等許多模塊都不再直接包含在express中,而是需要單獨(dú)添加相應(yīng)模塊。

express4 中操作 cookie 使用 cookie-parser 模塊(https://github.com/expressjs/cookie-parser )。

var express = require('express');
// 首先引入 cookie-parser 這個(gè)模塊
var cookieParser = require('cookie-parser');

var app = express();
app.listen(3000);

// 使用 cookieParser 中間件,cookieParser(secret, options)
// 其中 secret 用來(lái)加密 cookie 字符串(下面會(huì)提到 signedCookies)
// options 傳入上面介紹的 cookie 可選參數(shù)
app.use(cookieParser());

app.get('/', function (req, res) {
  // 如果請(qǐng)求中的 cookie 存在 isVisit, 則輸出 cookie
  // 否則,設(shè)置 cookie 字段 isVisit, 并設(shè)置過(guò)期時(shí)間為1分鐘
  if (req.cookies.isVisit) {
    console.log(req.cookies);
    res.send("再次歡迎訪問(wèn)");
  } else {
    res.cookie('isVisit', 1, {maxAge: 60 * 1000});
    res.send("歡迎第一次訪問(wèn)");
  }
});

session

cookie 雖然很方便,但是使用 cookie 有一個(gè)很大的弊端,cookie 中的所有數(shù)據(jù)在客戶(hù)端就可以被修改,數(shù)據(jù)非常容易被偽造,那么一些重要的數(shù)據(jù)就不能存放在 cookie 中了,而且如果 cookie 中數(shù)據(jù)字段太多會(huì)影響傳輸效率。為了解決這些問(wèn)題,就產(chǎn)生了 session,session 中的數(shù)據(jù)是保留在服務(wù)器端的。

session 的運(yùn)作通過(guò)一個(gè) session_id 來(lái)進(jìn)行。session_id 通常是存放在客戶(hù)端的 cookie 中,比如在 express 中,默認(rèn)是 connect.sid 這個(gè)字段,當(dāng)請(qǐng)求到來(lái)時(shí),服務(wù)端檢查 cookie 中保存的 session_id 并通過(guò)這個(gè) session_id 與服務(wù)器端的 session data 關(guān)聯(lián)起來(lái),進(jìn)行數(shù)據(jù)的保存和修改。

這意思就是說(shuō),當(dāng)你瀏覽一個(gè)網(wǎng)頁(yè)時(shí),服務(wù)端隨機(jī)產(chǎn)生一個(gè) 1024 比特長(zhǎng)的字符串,然后存在你 cookie 中的 connect.sid 字段中。當(dāng)你下次訪問(wèn)時(shí),cookie 會(huì)帶有這個(gè)字符串,然后瀏覽器就知道你是上次訪問(wèn)過(guò)的某某某,然后從服務(wù)器的存儲(chǔ)中取出上次記錄在你身上的數(shù)據(jù)。由于字符串是隨機(jī)產(chǎn)生的,而且位數(shù)足夠多,所以也不擔(dān)心有人能夠偽造。偽造成功的概率比坐在家里編程時(shí)被鄰居家的狗突然闖入并咬死的幾率還低。

session 可以存放在 1)內(nèi)存、2)cookie本身、3)redis 或 memcached 等緩存中,或者4)數(shù)據(jù)庫(kù)中。線(xiàn)上來(lái)說(shuō),緩存的方案比較常見(jiàn),存數(shù)據(jù)庫(kù)的話(huà),查詢(xún)效率相比前三者都太低,不推薦;cookie session 有安全性問(wèn)題,下面會(huì)提到。

express 中操作 session 要用到 express-session (https://github.com/expressjs/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ì)算 hash 值并放在 cookie 中,使產(chǎn)生的 signedCookie 防篡改。
  • cookie: 設(shè)置存放 session id 的 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)使用 uid2 這個(gè) npm 包。
  • rolling: 每個(gè)請(qǐng)求都重新設(shè)置一個(gè) cookie,默認(rèn)為 false。
  • resave: 即使 session 沒(méi)有被修改,也保存 session 值,默認(rèn)為 true。

1) 在內(nèi)存中存儲(chǔ) session

express-session 默認(rèn)使用內(nèi)存來(lái)存 session,對(duì)于開(kāi)發(fā)調(diào)試來(lái)說(shuō)很方便。

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', // 建議使用 128 個(gè)字符的隨機(jī)字符串
  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.session);
  }
});

2) 在 redis 中存儲(chǔ) session

session 存放在內(nèi)存中不方便進(jìn)程間共享,因此可以使用 redis 等緩存來(lái)存儲(chǔ) session。

假設(shè)你的機(jī)器是 4 核的,你使用了 4 個(gè)進(jìn)程在跑同一個(gè) node web 服務(wù),當(dāng)用戶(hù)訪問(wèn)進(jìn)程1時(shí),他被設(shè)置了一些數(shù)據(jù)當(dāng)做 session 存在內(nèi)存中。而下一次訪問(wèn)時(shí),他被負(fù)載均衡到了進(jìn)程2,則此時(shí)進(jìn)程2的內(nèi)存中沒(méi)有他的信息,認(rèn)為他是個(gè)新用戶(hù)。這就會(huì)導(dǎo)致用戶(hù)在我們服務(wù)中的狀態(tài)不一致。

使用 redis 作為緩存,可以使用 connect-redis 模塊(https://github.com/tj/connect-redis )來(lái)得到 redis 連接實(shí)例,然后在 session 中設(shè)置存儲(chǔ)方式為該實(shí)例。

var express = require('express');
var session = require('express-session');
var redisStore = require('connect-redis')(session);

var app = express();
app.listen(5000);

app.use(session({
  // 假如你不想使用 redis 而想要使用 memcached 的話(huà),代碼改動(dòng)也不會(huì)超過(guò) 5 行。
  // 這些 store 都遵循著統(tǒng)一的接口,凡是實(shí)現(xiàn)了那些接口的庫(kù),都可以作為 session 的 store 使用,比如都需要實(shí)現(xiàn) .get(keyString) 和 .set(keyString, value) 方法。
  // 編寫(xiě)自己的 store 也很簡(jiǎn)單
  store: new redisStore(),
  secret: 'somesecrettoken'
}));

app.get('/', function (req, res) {
  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)這里');
  }
});

我們可以運(yùn)行 redis-cli 查看結(jié)果,如圖可以看到 redis 中緩存結(jié)果。

http://wiki.jikexueyuan.com/project/node-lessons/images/16-1.png" alt="" />

各種存儲(chǔ)的利弊

上面我們說(shuō)到,session 的 store 有四個(gè)常用選項(xiàng):1)內(nèi)存 2)cookie 3)緩存 4)數(shù)據(jù)庫(kù)

其中,開(kāi)發(fā)環(huán)境存內(nèi)存就好了。一般的小程序?yàn)榱耸∈?,如果不涉及狀態(tài)共享的問(wèn)題,用內(nèi)存 session 也沒(méi)問(wèn)題。但內(nèi)存 session 除了省事之外,沒(méi)有別的好處。

cookie session 我們下面會(huì)提到,現(xiàn)在說(shuō)說(shuō)利弊。用 cookie session 的話(huà),是不用擔(dān)心狀態(tài)共享問(wèn)題的,因?yàn)?session 的 data 不是由服務(wù)器來(lái)保存,而是保存在用戶(hù)瀏覽器端,每次用戶(hù)訪問(wèn)時(shí),都會(huì)主動(dòng)帶上他自己的信息。當(dāng)然在這里,安全性之類(lèi)的,只要遵照最佳實(shí)踐來(lái),也是有保證的。它的弊端是增大了數(shù)據(jù)量傳輸,利端是方便。

緩存方式是最常用的方式了,即快,又能共享狀態(tài)。相比 cookie session 來(lái)說(shuō),當(dāng) session data 比較大的時(shí)候,可以節(jié)省網(wǎng)絡(luò)傳輸。推薦使用。

數(shù)據(jù)庫(kù) session。除非你很熟悉這一塊,知道自己要什么,否則還是老老實(shí)實(shí)用緩存吧。

signedCookie

上面都是講基礎(chǔ),現(xiàn)在講一些專(zhuān)業(yè)點(diǎn)的。

上面有提到

cookie 雖然很方便,但是使用 cookie 有一個(gè)很大的弊端,cookie 中的所有數(shù)據(jù)在客戶(hù)端就可以被修改,數(shù)據(jù)非常容易被偽造

其實(shí)不是這樣的,那只是為了方便理解才那么寫(xiě)。要知道,計(jì)算機(jī)領(lǐng)域有個(gè)名詞叫 簽名,專(zhuān)業(yè)點(diǎn)說(shuō),叫 信息摘要算法。

比如我們現(xiàn)在面臨著一個(gè)菜鳥(niǎo)開(kāi)發(fā)的網(wǎng)站,他用 cookie 來(lái)記錄登陸的用戶(hù)憑證。相應(yīng)的 cookie 長(zhǎng)這樣:dotcom_user=alsotang,它說(shuō)明現(xiàn)在的用戶(hù)是 alsotang 這個(gè)用戶(hù)。如果我在瀏覽器中裝個(gè)插件,把它改成 dotcom_user=ricardo,服務(wù)器一讀取,就會(huì)誤認(rèn)為我是 ricardo。然后我就可以進(jìn)行 ricardo 才能進(jìn)行的操作了。之前 web 開(kāi)發(fā)不成熟的時(shí)候,用這招甚至可以黑個(gè)網(wǎng)站下來(lái),把 cookie 改成 dotcom_user=admin 就行了,唉,那是個(gè)玩黑客的黃金年代啊。

OK,現(xiàn)在我有一些數(shù)據(jù),不想存在 session 中,想存在 cookie 中,怎么保證不被篡改呢?答案很簡(jiǎn)單,簽個(gè)名。

假設(shè)我的服務(wù)器有個(gè)秘密字符串,是 this_is_my_secret_and_fuck_you_all,我為用戶(hù) cookie 的 dotcom_user 字段設(shè)置了個(gè)值 alsotang。cookie 本應(yīng)是

{dotcom_user: 'alsotang'}

這樣的。

而如果我們簽個(gè)名,比如把 dotcom_user 的值跟我的 secret_string 做個(gè) sha1

sha1('this_is_my_secret_and_fuck_you_all' + 'alsotang') === '4850a42e3bc0d39c978770392cbd8dc2923e3d1d'

然后把 cookie 變成這樣

{
  dotcom_user: 'alsotang',
  'dotcom_user.sig': '4850a42e3bc0d39c978770392cbd8dc2923e3d1d',
}

這樣一來(lái),用戶(hù)就沒(méi)法偽造信息了。一旦它更改了 cookie 中的信息,則服務(wù)器會(huì)發(fā)現(xiàn) hash 校驗(yàn)的不一致。

畢竟他不懂我們的 secret_string 是什么,而暴力破解哈希值的成本太高。

cookie-session

上面一直提到 session 可以存在 cookie 中,現(xiàn)在來(lái)講講具體的思路。這里所涉及的專(zhuān)業(yè)名詞叫做 對(duì)稱(chēng)加密。

假設(shè)我們想在用戶(hù)的 cookie 中存 session data,使用一個(gè)名為 session_data 的字段。

var sessionData = {username: 'alsotang', age: 22, company: 'alibaba', location: 'hangzhou'}

這段信息的話(huà),可以將 sessionData 與我們的 secret_string 一起做個(gè)對(duì)稱(chēng)加密,存到 cookie 的 session_data 字段中,只要你的 secret_string 足夠長(zhǎng),那么攻擊者也是無(wú)法獲取實(shí)際 session 內(nèi)容的。對(duì)稱(chēng)加密之后的內(nèi)容對(duì)于攻擊者來(lái)說(shuō)相當(dāng)于一段亂碼。

而當(dāng)用戶(hù)下次訪問(wèn)時(shí),我們就可以用 secret_string 來(lái)解密 sessionData,得到我們需要的 session data。

signedCookies 跟 cookie-session 還是有區(qū)別的:

1)是前者信息可見(jiàn)不可篡改,后者不可見(jiàn)也不可篡改

2)是前者一般是長(zhǎng)期保存,而后者是 session cookie

cookie-session 的實(shí)現(xiàn)跟 signedCookies 差不多。

不過(guò) cookie-session 我個(gè)人建議不要使用,有受到回放攻擊的危險(xiǎn)。

回放攻擊指的是,比如一個(gè)用戶(hù),它現(xiàn)在有 100 積分,積分存在 session 中,session 保存在 cookie 中。他先復(fù)制下現(xiàn)在的這段 cookie,然后去發(fā)個(gè)帖子,扣掉了 20 積分,于是他就只有 80 積分了。而他現(xiàn)在可以將之前復(fù)制下的那段 cookie 再粘貼回去瀏覽器中,于是服務(wù)器在一些場(chǎng)景下會(huì)認(rèn)為他又有了 100 積分。

如果避免這種攻擊呢?這就需要引入一個(gè)第三方的手段來(lái)驗(yàn)證 cookie session,而驗(yàn)證所需的信息,一定不能存在 cookie 中。這么一來(lái),避免了這種攻擊后,使用 cookie session 的好處就蕩然無(wú)存了。如果為了避免攻擊而引入了緩存使用的話(huà),那不如把 cookie session 也一起放進(jìn)緩存中。

session cookie

初學(xué)者容易犯的一個(gè)錯(cuò)誤是,忘記了 session_id 在 cookie 中的存儲(chǔ)方式是 session cookie。即,當(dāng)用戶(hù)一關(guān)閉瀏覽器,瀏覽器 cookie 中的 session_id 字段就會(huì)消失。

常見(jiàn)的場(chǎng)景就是在開(kāi)發(fā)用戶(hù)登陸狀態(tài)保持時(shí)。

假如用戶(hù)在之前登陸了你的網(wǎng)站,你在他對(duì)應(yīng)的 session 中存了信息,當(dāng)他關(guān)閉瀏覽器再次訪問(wèn)時(shí),你還是不懂他是誰(shuí)。所以我們要在 cookie 中,也保存一份關(guān)于用戶(hù)身份的信息。

比如有這樣一個(gè)用戶(hù)

{username: 'alsotang', age: 22, company: 'alibaba', location: 'hangzhou'}

我們可以考慮把這四個(gè)字段的信息都存在 session 中,而在 cookie,我們用 signedCookies 來(lái)存?zhèn)€ username。

登陸的檢驗(yàn)過(guò)程偽代碼如下:


if (req.session.user) {
  // 獲取 user 并進(jìn)行下一步
  next()
} else if (req.signedCookies['username']) {
  // 如果存在則從數(shù)據(jù)庫(kù)中獲取這個(gè) username 的信息,并保存到 session 中
  getuser(function (err, user) {
    req.session.user = user;
    next();
  });
} else {
  // 當(dāng)做為登陸用戶(hù)處理
  next();
}

etag 當(dāng)做 session,保存 http 會(huì)話(huà)

很黑客的一種玩法:https://cnodejs.org/topic/5212d82d0a746c580b43d948