天天看點

cookie/session在nodes中的實戰

cookie 和 session

衆所周知,HTTP 是一個無狀态協定,是以用戶端每次送出請求時,下一次請求無法得知上一次請求所包含的狀态資料,如何能把一個使用者的狀态資料關聯起來呢?

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

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。

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 關聯起來,進行資料的儲存和修改。

這意思就是說,當你浏覽一個網頁時,服務端随機産生一個 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 生态中都有相應子產品的支援。
  • 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 )來得到 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 中緩存結果。

cookie/session在nodes中的實戰

各種存儲的利弊

上面我們說到,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 還是有差別的:

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 也一起放進緩存中。

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();
}           

etag 當做 session,儲存 http 會話

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

轉自http://wiki.jikexueyuan.com/project/node-lessons/cookie-session.html