Express-Session:SessionId 機(jī)制驅(qū)動(dòng)的一個(gè) Express 會(huì)話數(shù)據(jù)存儲(chǔ)庫
Express 是一個(gè) Node.js 的 Web 框架,提供對外服務(wù)器的功能。中間件則是 Express 提供的一種擴(kuò)展能力的插件機(jī)制。
express-session 就是 Express 的一個(gè)中間件。使用 sessionId 的機(jī)制,為用戶在網(wǎng)站訪問期間,提供會(huì)話數(shù)據(jù)的存儲(chǔ)支持。
技術(shù)實(shí)現(xiàn)上,express-session 就是為每個(gè)用戶生成唯一的一個(gè) sessionId(默認(rèn)通過名為 connect.sid 的 cookie 字段)并存儲(chǔ)在服務(wù)器上。在后續(xù)請求往返間,后端通過這個(gè) sessionId 就能拿到之前存儲(chǔ)的數(shù)據(jù),實(shí)現(xiàn)用戶訪問狀態(tài)的記憶。
注意:會(huì)話數(shù)據(jù)的存儲(chǔ)往往會(huì)借助文件系統(tǒng)或者數(shù)據(jù)庫系統(tǒng)(生產(chǎn)上通常叫緩沖數(shù)數(shù)據(jù)庫,比如 redis)等。express-session 管數(shù)據(jù)存儲(chǔ)叫 Store,默認(rèn)使用的是內(nèi)存(MemoryStore),不過生產(chǎn)上并不推薦。
圖片
安裝 & 簡單使用
express-session 依賴 express,因此使用時(shí)需要保證 express 也存在。
$ npm install express express-session
下面是一個(gè)簡單的使用。
var express = require('express')
var session = require('express-session')
var app = express()
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}))
secret 是必填項(xiàng),作為生成 sessio ID 的鹽值。resave、saveUninitialized 都是選填項(xiàng),不過由于這 2 個(gè)選項(xiàng)的默認(rèn)值會(huì)在未來版本修改,因此官方推薦顯式傳入。
express-session 是通過中間件方式注入到 express 應(yīng)用中的。經(jīng) express-session 處理后的請求實(shí)例 req 都包含一個(gè) .session 屬性,我們是通過在 .session 屬性上存儲(chǔ)信息,實(shí)現(xiàn)前后請求會(huì)話數(shù)據(jù)的保存的。
以下,我們將通過 2 個(gè)復(fù)雜一點(diǎn)的案例來介紹 express-session 的使用。
案例介紹
這里舉了 2 個(gè)例子,一個(gè)是統(tǒng)計(jì)用戶頁面訪問次數(shù),還有一個(gè)是用戶登錄的例子。
統(tǒng)計(jì)頁面訪問次數(shù)
我們先亮代碼(不是很多)。
var express = require('express')
var session = require('express-session')
var app = express()
// 1)
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}))
app.use(function (req, res, next) {
// 2)
if (!req.session.views) {
req.session.views = {}
}
// get the url pathname
var pathname = req.path
// 3)
// count the views
req.session.views[pathname] = (req.session.views[pathname] || 0) + 1
next()
})
app.get('/foo', function (req, res, next) {
res.send('you viewed this page ' + req.session.views['/foo'] + ' times')
})
app.get('/bar', function (req, res, next) {
res.send('you viewed this page ' + req.session.views['/bar'] + ' times')
})
app.listen(3000)
這里我們起了一個(gè)監(jiān)聽在 3000 端口的服務(wù)器,對訪問 /foo、/bar 頁面的次數(shù)做了統(tǒng)計(jì)。
- 首先express(session({ ... })) 一下,做好會(huì)話存儲(chǔ)準(zhǔn)備,調(diào)用后,會(huì)在每一次請求(req)添加一個(gè) .session 對象屬性
- 頁面訪問數(shù)據(jù)存儲(chǔ)在 req.session.views 對象屬性上,初次訪問時(shí)是沒有這個(gè)對象的,就創(chuàng)建({})
- 接下來獲取某個(gè)訪問路徑下(req.path)的訪問次數(shù)(+1),結(jié)束
用戶登錄
用戶登錄是一個(gè)稍微復(fù)雜一點(diǎn)的例子,分登錄和退出,我們拆開來講。
首先,我們針對用戶登錄和未登錄狀態(tài)來區(qū)別顯示首頁內(nèi)容:
- 用戶已登錄狀態(tài)下,顯示用戶名、暴露退出入口
- 用戶未登錄狀態(tài)下,顯示登錄表單,登錄請求發(fā)送至 /login
以下是代碼實(shí)現(xiàn):
var escapeHtml = require('escape-html')
var express = require('express')
var session = require('express-session')
var app = express()
// 1)
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}))
// 2.1) middleware to test if authenticated
function isAuthenticated (req, res, next) {
if (req.session.user) next()
else next('route')
}
// 2)
app.get('/', isAuthenticated, function (req, res) {
// this is only called when there is an authentication user due to isAuthenticated
res.send('hello, ' + escapeHtml(req.session.user) + '!' +
' <a href="/logout">Logout</a>')
})
// 3)
app.get('/', function (req, res) {
res.send('<form actinotallow="/login" method="post">' +
'Username: <input name="user"><br>' +
'Password: <input name="pass" type="password"><br>' +
'<input type="submit" text="Login"></form>')
})
// ...
app.listen(3000)
這里我們起了一個(gè)監(jiān)聽在 3000 端口的服務(wù)器,根據(jù)登錄狀態(tài)處理首頁展示邏輯。
- 還是老樣子,首先express(session({ ... })) 一下,做好會(huì)話存儲(chǔ)準(zhǔn)備,這一步會(huì)在每一次請求(req)添加一個(gè) .session 對象屬性
- 先跑第一個(gè) / 路徑邏輯,這一步會(huì)先經(jīng)過 isAuthenticated 中間件校驗(yàn)
- 用戶登錄后,我們會(huì)創(chuàng)建一個(gè) req.session.user 屬性存儲(chǔ)是用戶數(shù)據(jù),isAuthenticated 是檢查這個(gè)屬性又沒有的,有 req.session.user 話,就說明登陸了,展示用戶信息(next());沒有 req.session.user 的話,說明未登錄,則忽略用戶信息展示,跳轉(zhuǎn)至下一個(gè)路由處理(next('route'),也就是第 3 步)
- 經(jīng)過上一步,到這一步說明用戶未登錄,我們就發(fā)送一個(gè)登錄表單,讓用戶填寫。登錄表單包含 user、pass 字段信息。
接下來,我們來看看 /login 頁面的處理邏輯。
// ...
// 1)
app.post('/login', express.urlencoded({ extended: false }), function (req, res) {
// login logic to validate req.body.user and req.body.pass
// would be implemented here. for this example any combo works
// regenerate the session, which is good practice to help
// guard against forms of session fixation
// 1)
req.session.regenerate(function (err) {
if (err) next(err)
// store user information in session, typically a user id
// 2)
req.session.user = req.body.user
// save the session before redirection to ensure page
// load does not happen before session is saved
// 3)
req.session.save(function (err) {
if (err) return next(err)
// 4)
res.redirect('/')
})
})
})
// ...
- 為了能正確處理 <form> 表單提交數(shù)據(jù),我們使用 express.urlencoded({ extended: false }) 中間件將 form 表單數(shù)據(jù)收集到 req.body 上
- 我們一上來并沒有立即對 req.body.user/req.body.pass 進(jìn)行校驗(yàn),而是調(diào)用了 req.session.regenerate() 重新生成用戶會(huì)話 sessionId,這能避免會(huì)話固定攻擊(session fixation attack)
- 接下來,為了做簡單演示,我們沒有校驗(yàn)密碼,而是直接將提交的用戶名存儲(chǔ)下來(req.session.user = req.body.user)
- 在重定向會(huì)首頁之前,我們又調(diào)用了 req.session.save() 將新 sessionId 下的 user 信息同步給 Store(默認(rèn)是緩存,實(shí)際生產(chǎn)往往是一個(gè)緩存數(shù)據(jù)庫(像 redis))
- 最后,重定到首頁,這時(shí)候頁面就顯示登錄用戶名了
再來看看退出登錄(/logout)的邏輯。
app.get('/logout', function (req, res, next) {
// logout logic
// clear the user from the session object and save.
// this will ensure that re-using the old sessionId
// does not have a logged in user
// 1)
req.session.user = null
// 2)
req.session.save(function (err) {
if (err) next(err)
// regenerate the session, which is good practice to help
// guard against forms of session fixation
// 3)
req.session.regenerate(function (err) {
if (err) next(err)
// 4)
res.redirect('/')
})
})
})
- 首先,我們將 req.session.user 置為空
- 然后,req.session.save() 將上面的修改同步到 Store
- 接著,通過調(diào)用 req.session.regenerate() 重新生成 sessionId,這塊跟登錄一樣,是為了避免會(huì)話固定攻擊
- 最后,重定到首頁,這時(shí)候頁面就未登錄狀態(tài)下的登錄框了
總結(jié)
express-session 是用來為 express 框架提供會(huì)話緩存支持的一個(gè)中間件。技術(shù)上是通過使用 sessionId 機(jī)制提供會(huì)話記憶支持的。
本文分別列舉了 2 個(gè)案例來說明 express-session 的使用:訪問次數(shù)和用戶登錄。不過需要注意的是,不管是登錄還是退出,都要有一個(gè)新生成 sessionId 的過程(req.session.regenerate()),這是為了避免會(huì)話固定攻擊。