Express-Session:SessionId 機制驅動的一個 Express 會話數據存儲庫
Express 是一個 Node.js 的 Web 框架,提供對外服務器的功能。中間件則是 Express 提供的一種擴展能力的插件機制。
express-session 就是 Express 的一個中間件。使用 sessionId 的機制,為用戶在網站訪問期間,提供會話數據的存儲支持。
技術實現上,express-session 就是為每個用戶生成唯一的一個 sessionId(默認通過名為 connect.sid 的 cookie 字段)并存儲在服務器上。在后續請求往返間,后端通過這個 sessionId 就能拿到之前存儲的數據,實現用戶訪問狀態的記憶。
注意:會話數據的存儲往往會借助文件系統或者數據庫系統(生產上通常叫緩沖數數據庫,比如 redis)等。express-session 管數據存儲叫 Store,默認使用的是內存(MemoryStore),不過生產上并不推薦。
圖片
安裝 & 簡單使用
express-session 依賴 express,因此使用時需要保證 express 也存在。
$ npm install express express-session
下面是一個簡單的使用。
var express = require('express')
var session = require('express-session')
var app = express()
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}))
secret 是必填項,作為生成 sessio ID 的鹽值。resave、saveUninitialized 都是選填項,不過由于這 2 個選項的默認值會在未來版本修改,因此官方推薦顯式傳入。
express-session 是通過中間件方式注入到 express 應用中的。經 express-session 處理后的請求實例 req 都包含一個 .session 屬性,我們是通過在 .session 屬性上存儲信息,實現前后請求會話數據的保存的。
以下,我們將通過 2 個復雜一點的案例來介紹 express-session 的使用。
案例介紹
這里舉了 2 個例子,一個是統計用戶頁面訪問次數,還有一個是用戶登錄的例子。
統計頁面訪問次數
我們先亮代碼(不是很多)。
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)
這里我們起了一個監聽在 3000 端口的服務器,對訪問 /foo、/bar 頁面的次數做了統計。
- 首先express(session({ ... })) 一下,做好會話存儲準備,調用后,會在每一次請求(req)添加一個 .session 對象屬性
- 頁面訪問數據存儲在 req.session.views 對象屬性上,初次訪問時是沒有這個對象的,就創建({})
- 接下來獲取某個訪問路徑下(req.path)的訪問次數(+1),結束
用戶登錄
用戶登錄是一個稍微復雜一點的例子,分登錄和退出,我們拆開來講。
首先,我們針對用戶登錄和未登錄狀態來區別顯示首頁內容:
- 用戶已登錄狀態下,顯示用戶名、暴露退出入口
- 用戶未登錄狀態下,顯示登錄表單,登錄請求發送至 /login
以下是代碼實現:
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)
這里我們起了一個監聽在 3000 端口的服務器,根據登錄狀態處理首頁展示邏輯。
- 還是老樣子,首先express(session({ ... })) 一下,做好會話存儲準備,這一步會在每一次請求(req)添加一個 .session 對象屬性
- 先跑第一個 / 路徑邏輯,這一步會先經過 isAuthenticated 中間件校驗
- 用戶登錄后,我們會創建一個 req.session.user 屬性存儲是用戶數據,isAuthenticated 是檢查這個屬性又沒有的,有 req.session.user 話,就說明登陸了,展示用戶信息(next());沒有 req.session.user 的話,說明未登錄,則忽略用戶信息展示,跳轉至下一個路由處理(next('route'),也就是第 3 步)
- 經過上一步,到這一步說明用戶未登錄,我們就發送一個登錄表單,讓用戶填寫。登錄表單包含 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> 表單提交數據,我們使用 express.urlencoded({ extended: false }) 中間件將 form 表單數據收集到 req.body 上
- 我們一上來并沒有立即對 req.body.user/req.body.pass 進行校驗,而是調用了 req.session.regenerate() 重新生成用戶會話 sessionId,這能避免會話固定攻擊(session fixation attack)
- 接下來,為了做簡單演示,我們沒有校驗密碼,而是直接將提交的用戶名存儲下來(req.session.user = req.body.user)
- 在重定向會首頁之前,我們又調用了 req.session.save() 將新 sessionId 下的 user 信息同步給 Store(默認是緩存,實際生產往往是一個緩存數據庫(像 redis))
- 最后,重定到首頁,這時候頁面就顯示登錄用戶名了
再來看看退出登錄(/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
- 接著,通過調用 req.session.regenerate() 重新生成 sessionId,這塊跟登錄一樣,是為了避免會話固定攻擊
- 最后,重定到首頁,這時候頁面就未登錄狀態下的登錄框了
總結
express-session 是用來為 express 框架提供會話緩存支持的一個中間件。技術上是通過使用 sessionId 機制提供會話記憶支持的。
本文分別列舉了 2 個案例來說明 express-session 的使用:訪問次數和用戶登錄。不過需要注意的是,不管是登錄還是退出,都要有一個新生成 sessionId 的過程(req.session.regenerate()),這是為了避免會話固定攻擊。