Node.js 16:《cookie 與 session》

2018-08-07 15:23 更新

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

比如在淘寶的某個頁面中,你進行了登陸操作。當你跳轉到商品頁時,服務端如何知道你是已經登陸的狀態(tài)?

cookie

首先產生了 cookie 這門技術來解決這個問題,cookie 是 http 協(xié)議的一部分,它的處理分為如下幾步:

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

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

  • path:表示 cookie 影響到的路徑,匹配該路徑才發(fā)送這個 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。

express 中的 cookie

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

express4 中操作 cookie 使用 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("歡迎第一次訪問");
  }
});

session

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

session 的運作通過一個 session_id 來進行。session_id 通常是存放在客戶端的 cookie 中,比如在 express 中,默認是 connect.sid 這個字段,當請求到來時,服務端檢查 cookie 中保存的 session_id 并通過這個 session_id 與服務器端的 session data 關聯(lián)起來,進行數據的保存和修改。

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

session 可以存放在 1)內存、2)cookie本身、3)redis 或 memcached 等緩存中,或者4)數據庫中。線上來說,緩存的方案比較常見,存數據庫的話,查詢效率相比前三者都太低,不推薦;cookie session 有安全性問題,下面會提到。

express 中操作 session 要用到 express-session (https://github.com/expressjs/session ) 這個模塊,主要的方法就是session(options),其中 options 中包含可選參數,主要有:

  • name: 設置 cookie 中,保存 session 的字段名稱,默認為 connect.sid 。
  • store: session 的存儲方式,默認存放在內存中,也可以使用 redis,mongodb 等。express 生態(tài)中都有相應模塊的支持。
  • 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,對于開發(fā)調試來說很方便。

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的內存中沒有他的信息,認為他是個新用戶。這就會導致用戶在我們服務中的狀態(tài)不一致。

使用 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 都遵循著統(tǒng)一的接口,凡是實現(xiàn)了那些接口的庫,都可以作為 session 的 store 使用,比如都需要實現(xiàn) .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)數據庫

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

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

緩存方式是最常用的方式了,即快,又能共享狀態(tài)。相比 cookie session 來說,當 session data 比較大的時候,可以節(jié)省網絡傳輸。推薦使用。

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

signedCookie

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

上面有提到

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

其實不是這樣的,那只是為了方便理解才那么寫。要知道,計算機領域有個名詞叫 簽名,專業(yè)點說,叫 信息摘要算法

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

OK,現(xiàn)在我有一些數據,不想存在 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 中的信息,則服務器會發(fā)現(xiàn) hash 校驗的不一致。

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

cookie-session

上面一直提到 session 可以存在 cookie 中,現(xiàn)在來講講具體的思路。這里所涉及的專業(yè)名詞叫做 對稱加密。 假設我們想在用戶的 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 還是有區(qū)別的: 1)是前者信息可見不可篡改,后者不可見也不可篡改 2)是前者一般是長期保存,而后者是 session cookie

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

不過 cookie-session 我個人建議不要使用,有受到回放攻擊的危險。

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

如果避免這種攻擊呢?這就需要引入一個第三方的手段來驗證 cookie session,而驗證所需的信息,一定不能存在 cookie 中。這么一來,避免了這種攻擊后,使用 cookie session 的好處就蕩然無存了。如果為了避免攻擊而引入了緩存使用的話,那不如把 cookie session 也一起放進緩存中。

session cookie

初學者容易犯的一個錯誤是,忘記了 session_id 在 cookie 中的存儲方式是 session cookie。即,當用戶一關閉瀏覽器,瀏覽器 cookie 中的 session_id 字段就會消失。

常見的場景就是在開發(fā)用戶登陸狀態(tài)保持時。

假如用戶在之前登陸了你的網站,你在他對應的 session 中存了信息,當他關閉瀏覽器再次訪問時,你還是不懂他是誰。所以我們要在 cookie 中,也保存一份關于用戶身份的信息。

比如有這樣一個用戶

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

我們可以考慮把這四個字段的信息都存在 session 中,而在 cookie,我們用 signedCookies 來存?zhèn)€ 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();
}

etag 當做 session,保存 http 會話

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

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號