眾所周知,HTTP是一種無狀態的協議,也就是說:當客戶端每次發出請求時,下一次請求無法得知上一次請求的狀態(所包含的狀態數據)。一旦數據交換完畢,客戶端與服務器端的連接就會關閉,再次交換數據需要建立新的連接。那么如何才能把一個用戶的狀態數據保存或者關聯起來?由此產生了cookie和session這兩門技術來解決這一問題。
會話(Session)跟蹤是Web程序中常用的技術,用來跟蹤用戶的整個會話。常用的會話跟蹤技術是Cookie與Session。Cookie通過在客戶端記錄信息確定用戶身份,Session通過在服務器端記錄信息確定用戶身份。
* * * * *
# cookie
首先產生了 cookie 這門技術來解決這個問題,由于HTTP是一種無狀態的協議,服務器單從網絡連接上無從知道客戶身份。怎么辦呢?就給客戶端們頒發一個通行證吧,每人一個,無論誰訪問都必須攜帶自己通行證。這樣服務器就能從通行證上確認客戶身份了。這就是Cookie的工作原理。
cookie是一個實際存在的東西,它是 http 協議的一部分。
## 處理流程(B代表瀏覽器,S代表服務器):
1. B首次請求S
2. S將cookie放在響應頭中發送給B,具體是response頭中的`Set-Cookie`
3. B接收到cookie之后,將cookie進行存儲
4. 之后每次B請求S時,將cookie放在請求頭中發送給S,具體是request頭中的`Cookie`
## 使用場景
1. 非重要的用戶信息
2. 瀏覽記錄(例如京東、天貓等都有瀏覽記錄的功能)
3. 猜你喜歡
S通過分析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。
## cookie在node.js(express)中的應用
express 在 4.x 版本之后,session管理和cookies等許多模塊都不再直接包含在express中,而是需要單獨添加相應模塊。
### 下載安裝
express4 中操作cookie需要使用 `cookie-parser`模塊(https://github.com/expressjs/cookie-parser )。
```
cnpm i cookie-parser -S
```
### 使用步驟
1. 加載express模塊
2. 創建express實例app
3. 加載cookie-parser模塊
4. app使用cookieParser
5. `res.cookie()`設置cookie,`req.cookie `就可以訪問瀏覽器攜帶的cookie了,其中`req.signedCookies`是訪問的是加密模式下的cookie。
```
let express = require('express')
// 首先引入 cookie-parser 這個模塊
let cookieParser = require('cookie-parser')
let app = express()
app.listen(3000)
/*
使用 cookieParser 中間件,cookieParser(secret, options)
其中 secret 用來加密 cookie 字符串(下面會提到 signedCookies)
options 傳入上面介紹的 cookie 可選參數
*/
app.use(cookieParser())
app.get('/', (req, res) => {
// Cookies that have not been signed
console.log('Cookies: ', req.cookies)
// Cookies that have been signed
console.log('Signed Cookies: ', req.signedCookies)
})
```
# session
除了使用cookie,Web應用程序中還經常使用session來記錄客戶端狀態。session是服務器端使用的一種記錄客戶端狀態的機制,使用上比cookie簡單一些,相應的也增加了服務器的存儲壓力。
## 什么是session?
Session是另一種記錄客戶狀態的機制,不同的是Cookie保存在客戶端瀏覽器中,而Session保存在服務器上。客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄在服務器上。這就是Session。客戶端瀏覽器再次訪問時只需要從該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 關聯起來,進行數據的保存和修改。
## 存儲位置
session 可以存放在 :
1. 內存
2. cookie本身
3. redis 或 memcached 等緩存中
4. 數據庫
5. 文件
6. 集群
線上來說,緩存的方案比較常見,存數據庫的話,查詢效率相比前三者都太低,不推薦。cookie session 有安全性問題,下面會提到。
## session在node.js(express)中的應用
### 下載安裝
express 中操作 session 要用到`express-session`模塊 (https://github.com/expressjs/session )
```
cnpm i express-session -S
```
這個模塊,主要的方法就是 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。
## session存儲舉例
### 在內存中存儲 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);
}
});
```
### 在 redis 中存儲 session
session 存放在內存中不方便進程間共享,因此可以使用 redis 等緩存來存儲 session。
假設你的機器是 4 核的,你使用了 4 個進程在跑同一個 node web 服務,當用戶訪問進程1時,他被設置了一些數據當做 session 存在內存中。而下一次訪問時,他被負載均衡到了進程2,則此時進程2的內存中沒有他的信息,認為他是個新用戶。這就會導致用戶在我們服務中的狀態不一致。
使用 redis 作為緩存,可以使用`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 中緩存結果。
## 各種存儲的利弊
上面我們說到,session 的 store 有四個常用選項:1)內存 2)cookie 3)緩存 4)數據庫
其中,開發環境存內存就好了。一般的小程序為了省事,如果不涉及狀態共享的問題,用內存 session 也沒問題。但內存 session 除了省事之外,沒有別的好處。
cookie session 我們下面會提到,現在說說利弊。用 cookie session 的話,是不用擔心狀態共享問題的,因為 session 的 data 不是由服務器來保存,而是保存在用戶瀏覽器端,每次用戶訪問時,都會主動帶上他自己的信息。當然在這里,安全性之類的,只要遵照最佳實踐來,也是有保證的。它的弊端是增大了數據量傳輸,利端是方便。
緩存方式是最常用的方式了,即快,又能共享狀態。相比 cookie session 來說,當 session data 比較大的時候,可以節省網絡傳輸。推薦使用。
數據庫 session。除非你很熟悉這一塊,知道自己要什么,否則還是老老實實用緩存吧。
### 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 是什么,而暴力破解哈希值的成本太高。
### cookie-session
上面一直提到 session 可以存在 cookie 中,現在來講講具體的思路。這里所涉及的專業名詞叫做 對稱加密。
假設我們想在用戶的 cookie 中存 session data,使用一個名為 session_data 的字段。
存
```
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 還是有區別的:
- 是前者信息可見不可篡改,后者不可見也不可篡改
- 是前者一般是長期保存,而后者是 session cookie
cookie-session 的實現跟 signedCookies 差不多。
不過 cookie-session 我個人建議不要使用,有受到回放攻擊的危險。
回放攻擊指的是,比如一個用戶,它現在有 100 積分,積分存在 session 中,session 保存在 cookie 中。他先復制下現在的這段 cookie,然后去發個帖子,扣掉了 20 積分,于是他就只有 80 積分了。而他現在可以將之前復制下的那段 cookie 再粘貼回去瀏覽器中,于是服務器在一些場景下會認為他又有了 100 積分。
如果避免這種攻擊呢?這就需要引入一個第三方的手段來驗證 cookie session,而驗證所需的信息,一定不能存在 cookie 中。這么一來,避免了這種攻擊后,使用 cookie session 的好處就蕩然無存了。如果為了避免攻擊而引入了緩存使用的話,那不如把 cookie session 也一起放進緩存中。
### 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();
}
```