眾所周知,HTTP 是一個無狀態協議,所以客戶端每次發出請求時,下一次請求無法得知上一次請求所包含的狀態數據,如何能把一個用戶的狀態數據關聯起來呢?
比如在淘寶的某個頁面中,你進行了登陸操作。當你跳轉到商品頁時,服務端如何知道你是已經登陸的狀態?
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#cookie)cookie
首先產生了 cookie 這門技術來解決這個問題,cookie 是 http 協議的一部分,它的處理分為如下幾步:
* 服務器向客戶端發送 cookie。
* 通常使用 HTTP 協議規定的 set-cookie 頭操作。
* 規范規定 cookie 的格式為 name = value 格式,且必須包含這部分。
* 瀏覽器將 cookie 保存。
* 每次請求瀏覽器都會將 cookie 發向服務器。
其他可選的 cookie 參數會影響將 cookie 發送給服務器端的過程,主要有以下幾種:
* path:表示 cookie 影響到的路徑,匹配該路徑才發送這個 cookie。
* expires 和 maxAge:告訴瀏覽器這個 cookie 什么時候過期,expires 是 UTC 格式時間,maxAge 是 cookie 多久后過期的相對時間。當不設置這兩個選項時,會產生 session cookie,session cookie 是 transient 的,當用戶關閉瀏覽器時,就被清除。一般用來保存 session 的 session_id。
* secure:當 secure 值為 true 時,cookie 在 HTTP 中是無效,在 HTTPS 中才有效。
* httpOnly:瀏覽器不允許腳本操作 document.cookie 去更改 cookie。一般情況下都應該設置這個為 true,這樣可以避免被 xss 攻擊拿到 cookie。
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#express-中的-cookie)express 中的 cookie
express 在 4.x 版本之后,session管理和cookies等許多模塊都不再直接包含在express中,而是需要單獨添加相應模塊。
express4 中操作 cookie 使用?`cookie-parser`?模塊([https://github.com/expressjs/cookie-parser](https://github.com/expressjs/cookie-parser)?)。
~~~
var express = require('express');
// 首先引入 cookie-parser 這個模塊
var cookieParser = require('cookie-parser');
var app = express();
app.listen(3000);
// 使用 cookieParser 中間件,cookieParser(secret, options)
// 其中 secret 用來加密 cookie 字符串(下面會提到 signedCookies)
// options 傳入上面介紹的 cookie 可選參數
app.use(cookieParser());
app.get('/', function (req, res) {
// 如果請求中的 cookie 存在 isVisit, 則輸出 cookie
// 否則,設置 cookie 字段 isVisit, 并設置過期時間為1分鐘
if (req.cookies.isVisit) {
console.log(req.cookies);
res.send("再次歡迎訪問");
} else {
res.cookie('isVisit', 1, {maxAge: 60 * 1000});
res.send("歡迎第一次訪問");
}
});
~~~
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#session)session
cookie 雖然很方便,但是使用 cookie 有一個很大的弊端,cookie 中的所有數據在客戶端就可以被修改,數據非常容易被偽造,那么一些重要的數據就不能存放在 cookie 中了,而且如果 cookie 中數據字段太多會影響傳輸效率。為了解決這些問題,就產生了 session,session 中的數據是保留在服務器端的。
session 的運作通過一個?`session_id`?來進行。`session_id`?通常是存放在客戶端的 cookie 中,比如在 express 中,默認是?`connect.sid`?這個字段,當請求到來時,服務端檢查 cookie 中保存的 session_id 并通過這個 session_id 與服務器端的 session data 關聯起來,進行數據的保存和修改。
這意思就是說,當你瀏覽一個網頁時,服務端隨機產生一個 1024 比特長的字符串,然后存在你 cookie 中的?`connect.sid`字段中。當你下次訪問時,cookie 會帶有這個字符串,然后瀏覽器就知道你是上次訪問過的某某某,然后從服務器的存儲中取出上次記錄在你身上的數據。由于字符串是隨機產生的,而且位數足夠多,所以也不擔心有人能夠偽造。偽造成功的概率比坐在家里編程時被鄰居家的狗突然闖入并咬死的幾率還低。
session 可以存放在 1)內存、2)cookie本身、3)redis 或 memcached 等緩存中,或者4)數據庫中。線上來說,緩存的方案比較常見,存數據庫的話,查詢效率相比前三者都太低,不推薦;cookie session 有安全性問題,下面會提到。
express 中操作 session 要用到?`express-session`?([https://github.com/expressjs/session](https://github.com/expressjs/session)?) 這個模塊,主要的方法就是`session(options)`,其中 options 中包含可選參數,主要有:
* name: 設置 cookie 中,保存 session 的字段名稱,默認為?`connect.sid`?。
* store: session 的存儲方式,默認存放在內存中,也可以使用 redis,mongodb 等。express 生態中都有相應模塊的支持。
* secret: 通過設置的 secret 字符串,來計算 hash 值并放在 cookie 中,使產生的 signedCookie 防篡改。
* cookie: 設置存放 session id 的 cookie 的相關選項,默認為
* (default: { path: '/', httpOnly: true, secure: false, maxAge: null })
* genid: 產生一個新的 session_id 時,所使用的函數, 默認使用?`uid2`?這個 npm 包。
* rolling: 每個請求都重新設置一個 cookie,默認為 false。
* resave: 即使 session 沒有被修改,也保存 session 值,默認為 true。
1) 在內存中存儲 session
`express-session`?默認使用內存來存 session,對于開發調試來說很方便。
~~~
var express = require('express');
// 首先引入 express-session 這個模塊
var session = require('express-session');
var app = express();
app.listen(5000);
// 按照上面的解釋,設置 session 的可選參數
app.use(session({
secret: 'recommand 128 bytes random string', // 建議使用 128 個字符的隨機字符串
cookie: { maxAge: 60 * 1000 }
}));
app.get('/', function (req, res) {
// 檢查 session 中的 isVisit 字段
// 如果存在則增加一次,否則為 session 設置 isVisit 字段,并初始化為 1。
if(req.session.isVisit) {
req.session.isVisit++;
res.send('<p>第 ' + req.session.isVisit + '次來此頁面</p>');
} else {
req.session.isVisit = 1;
res.send("歡迎第一次來這里");
console.log(req.session);
}
});
~~~
2) 在 redis 中存儲 session
session 存放在內存中不方便進程間共享,因此可以使用 redis 等緩存來存儲 session。
假設你的機器是 4 核的,你使用了 4 個進程在跑同一個 node web 服務,當用戶訪問進程1時,他被設置了一些數據當做 session 存在內存中。而下一次訪問時,他被負載均衡到了進程2,則此時進程2的內存中沒有他的信息,認為他是個新用戶。這就會導致用戶在我們服務中的狀態不一致。
使用 redis 作為緩存,可以使用?`connect-redis`?模塊([https://github.com/tj/connect-redis](https://github.com/tj/connect-redis)?)來得到 redis 連接實例,然后在 session 中設置存儲方式為該實例。
~~~
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 的話,代碼改動也不會超過 5 行。
// 這些 store 都遵循著統一的接口,凡是實現了那些接口的庫,都可以作為 session 的 store 使用,比如都需要實現 .get(keyString) 和 .set(keyString, value) 方法。
// 編寫自己的 store 也很簡單
store: new redisStore(),
secret: 'somesecrettoken'
}));
app.get('/', function (req, res) {
if(req.session.isVisit) {
req.session.isVisit++;
res.send('<p>第 ' + req.session.isVisit + '次來到此頁面</p>');
} else {
req.session.isVisit = 1;
res.send('歡迎第一次來這里');
}
});
~~~
我們可以運行?`redis-cli`?查看結果,如圖可以看到 redis 中緩存結果。
[](https://github.com/Ricardo-Li/node-lessons/blob/master/lesson16/1.png)
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#各種存儲的利弊)各種存儲的利弊
上面我們說到,session 的 store 有四個常用選項:1)內存 2)cookie 3)緩存 4)數據庫
其中,開發環境存內存就好了。一般的小程序為了省事,如果不涉及狀態共享的問題,用內存 session 也沒問題。但內存 session 除了省事之外,沒有別的好處。
cookie session 我們下面會提到,現在說說利弊。用 cookie session 的話,是不用擔心狀態共享問題的,因為 session 的 data 不是由服務器來保存,而是保存在用戶瀏覽器端,每次用戶訪問時,都會主動帶上他自己的信息。當然在這里,安全性之類的,只要遵照最佳實踐來,也是有保證的。它的弊端是增大了數據量傳輸,利端是方便。
緩存方式是最常用的方式了,即快,又能共享狀態。相比 cookie session 來說,當 session data 比較大的時候,可以節省網絡傳輸。推薦使用。
數據庫 session。除非你很熟悉這一塊,知道自己要什么,否則還是老老實實用緩存吧。
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#signedcookie)signedCookie
上面都是講基礎,現在講一些專業點的。
上面有提到
> cookie 雖然很方便,但是使用 cookie 有一個很大的弊端,cookie 中的所有數據在客戶端就可以被修改,數據非常容易被偽造
其實不是這樣的,那只是為了方便理解才那么寫。要知道,計算機領域有個名詞叫?**簽名**,專業點說,叫?**信息摘要算法**。
比如我們現在面臨著一個菜鳥開發的網站,他用 cookie 來記錄登陸的用戶憑證。相應的 cookie 長這樣:`dotcom_user=alsotang`,它說明現在的用戶是 alsotang 這個用戶。如果我在瀏覽器中裝個插件,把它改成`dotcom_user=ricardo`,服務器一讀取,就會誤認為我是 ricardo。然后我就可以進行 ricardo 才能進行的操作了。之前 web 開發不成熟的時候,用這招甚至可以黑個網站下來,把 cookie 改成?`dotcom_user=admin`?就行了,唉,那是個玩黑客的黃金年代啊。
OK,現在我有一些數據,不想存在 session 中,想存在 cookie 中,怎么保證不被篡改呢?答案很簡單,簽個名。
假設我的服務器有個秘密字符串,是?`this_is_my_secret_and_fuck_you_all`,我為用戶 cookie 的?`dotcom_user`?字段設置了個值?`alsotang`。cookie 本應是
~~~
{dotcom_user: 'alsotang'}
~~~
這樣的。
而如果我們簽個名,比如把?`dotcom_user`?的值跟我的 secret_string 做個 sha1
`sha1('this_is_my_secret_and_fuck_you_all' + 'alsotang') === '4850a42e3bc0d39c978770392cbd8dc2923e3d1d'`
然后把 cookie 變成這樣
~~~
{
dotcom_user: 'alsotang',
'dotcom_user.sig': '4850a42e3bc0d39c978770392cbd8dc2923e3d1d',
}
~~~
這樣一來,用戶就沒法偽造信息了。一旦它更改了 cookie 中的信息,則服務器會發現 hash 校驗的不一致。
畢竟他不懂我們的 secret_string 是什么,而暴力破解哈希值的成本太高。
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#cookie-session)cookie-session
~~上面一直提到 session 可以存在 cookie 中,現在來講講具體的思路。這里所涉及的專業名詞叫做 對稱加密。 假設我們想在用戶的 cookie 中存 session data,使用一個名為 `session_data` 的字段。 存 ```js var sessionData = {username: 'alsotang', age: 22, company: 'alibaba', location: 'hangzhou'} ``` 這段信息的話,可以將 `sessionData` 與我們的 `secret_string` 一起做個對稱加密,存到 cookie 的 `session_data` 字段中,只要你的 `secret_string` 足夠長,那么攻擊者也是無法獲取實際 session 內容的。對稱加密之后的內容對于攻擊者來說相當于一段亂碼。 而當用戶下次訪問時,我們就可以用 `secret_string` 來解密 `sessionData`,得到我們需要的 session data。 signedCookies 跟 cookie-session 還是有區別的: 1)是前者信息可見不可篡改,后者不可見也不可篡改 2)是前者一般是長期保存,而后者是 session cookie~~
cookie-session 的實現跟 signedCookies 差不多。
不過 cookie-session 我個人建議不要使用,有受到回放攻擊的危險。
回放攻擊指的是,比如一個用戶,它現在有 100 積分,積分存在 session 中,session 保存在 cookie 中。他先復制下現在的這段 cookie,然后去發個帖子,扣掉了 20 積分,于是他就只有 80 積分了。而他現在可以將之前復制下的那段 cookie 再粘貼回去瀏覽器中,于是服務器在一些場景下會認為他又有了 100 積分。
如果避免這種攻擊呢?這就需要引入一個第三方的手段來驗證 cookie session,而驗證所需的信息,一定不能存在 cookie 中。這么一來,避免了這種攻擊后,使用 cookie session 的好處就蕩然無存了。如果為了避免攻擊而引入了緩存使用的話,那不如把 cookie session 也一起放進緩存中。
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#session-cookie)session cookie
初學者容易犯的一個錯誤是,忘記了 session_id 在 cookie 中的存儲方式是 session cookie。即,當用戶一關閉瀏覽器,瀏覽器 cookie 中的 session_id 字段就會消失。
常見的場景就是在開發用戶登陸狀態保持時。
假如用戶在之前登陸了你的網站,你在他對應的 session 中存了信息,當他關閉瀏覽器再次訪問時,你還是不懂他是誰。所以我們要在 cookie 中,也保存一份關于用戶身份的信息。
比如有這樣一個用戶
~~~
{username: 'alsotang', age: 22, company: 'alibaba', location: 'hangzhou'}
~~~
我們可以考慮把這四個字段的信息都存在 session 中,而在 cookie,我們用 signedCookies 來存個 username。
登陸的檢驗過程偽代碼如下:
~~~
if (req.session.user) {
// 獲取 user 并進行下一步
next()
} else if (req.signedCookies['username']) {
// 如果存在則從數據庫中獲取這個 username 的信息,并保存到 session 中
getuser(function (err, user) {
req.session.user = user;
next();
});
} else {
// 當做為登陸用戶處理
next();
}
~~~
### [](https://github.com/alsotang/node-lessons/tree/master/lesson16#etag-當做-session保存-http-會話)etag 當做 session,保存 http 會話
很黑客的一種玩法:[https://cnodejs.org/topic/5212d82d0a746c580b43d948](https://cnodejs.org/topic/5212d82d0a746c580b43d948)
- 關于
- Lesson 0: 《搭建 Node.js 開發環境》
- Lesson 1: 《一個最簡單的 express 應用》
- Lesson 2: 《學習使用外部模塊》
- Lesson 3: 《使用 superagent 與 cheerio 完成簡單爬蟲》
- Lesson 4: 《使用 eventproxy 控制并發》
- Lesson 5: 《使用 async 控制并發》
- Lesson 6: 《測試用例:mocha,should,istanbul》
- Lesson 7: 《瀏覽器端測試:mocha,chai,phantomjs》
- Lesson 8: 《測試用例:supertest》
- Lesson 9: 《正則表達式》
- Lesson 10: 《benchmark 怎么寫》
- Lesson 11: 《作用域與閉包:this,var,(function () {})》
- Lesson 12: 《線上部署:heroku》
- Lesson 13: 《持續集成平臺:travis》
- Lesson 14: 《js 中的那些最佳實踐》
- Lesson 15: 《Mongodb 與 Mongoose 的使用》
- Lesson 16: 《cookie 與 session》
- Lesson 17: 《使用 promise 替代回調函數》