[Node.js] cookie-session驗證原理以及express-session套件使用

[Node.js] cookie-session驗證原理以及express-session套件使用

Johnny

Johnny的轉職工程師筆記

Johnny

·

Follow

Published in

Johnny的轉職工程師筆記

·

·

Feb 2, 2021

10 min read

cookie-session的驗證機制

HTTP的限制

之前在什麼是HTTP?有寫過,因為HTTP是stateless的,所以server並不會保存前一次request的任何資訊。也就是說,如果不做任何額外的設定的話,如果我今天登入了某個網站,只要按了重新整理(再發一次get request),這個網站就會要求我重新登入一次,因為server已經不記得我了。

如何保存使用者狀態

也是因為要解決這個問題,我們後端工程師需要讓client request附上一個「憑證」,這樣server端才有辦法記住已經登入過的人,讓使用者不用一直無限的重新登入。所以,可以想像client端和server端都需要有個儲存空間來存放這個憑證,才能做出驗證/登入的功能。

在client端,瀏覽器提供了一個叫做cookie的儲存空間,在server端,則是使用session store去做儲存。值得注意的是,cookie在瀏覽器是透過key-value pair去儲存資料,在server端的session store因為沒有統一規範,不管是存放方式還是資料結構,其實怎麼實作都可以。但身為菜鳥,使用現成的工具一定是比自己造輪子好多了XD

透過cookie-session驗證使用者流程如下:

使用者登入 -> 驗證資料庫內的帳號密碼 -> 通過後在伺服器端把使用者資訊存入session store,並生成session ID作為索引-> 給使用者session ID存放在瀏覽器的cookie -> 下次使用者進入頁面時,比對cookie內的session ID和伺服器端一不一樣 -> 如果一樣則回傳使用者資訊,不一樣則不讓使用者進入頁面。

使用express-session實作

先附上自己用express-session寫的帳號登入頁面:

接下來一步步解釋實作流程和背後的原理,這篇文只會專注在session-cookie驗證的部分。

引入express-session套件

剛剛提到不需要自己造輪子,如果是寫express的話,最常用的套件就是express-session了:

express-session是一個middleware,需要把它寫在路由之前:

const session = require('express-session')

app.use(session({
secret: 'mySecret',
name: 'user', // optional
saveUninitialized: false,
resave: true,
})

// 後面再寫路由...

  • secret: 用來簽名存放在cookie的sessionID
  • name: 存放在cookie的key,如果不寫的話預設是connect.sid
  • saveUninitialized: 這個如果設定是true,會把「還沒修改過的」session就存進session store。以登入的例子來說,就是使用者還沒登入,我還沒把使用者資訊寫入session,就先存放了session在session store。設定為false可以避免存放太多空的session進入session store。另外,如果設為false,session在還沒被修改前也不會被存入cookie。
  • resave: 如果設定為true,則在一個request中,無論session有沒有被修改過,都會強制保存原本的session在session store。會有這個設定是因為每個session store會有不一樣的配置,有些會定期去清理session,如果不想要session被清理掉的話,就要把這個設定為true。

把express-session寫在路由之前,所有的request都會生成一個session並可以透過req.session這個變數來取得session內容,以及req.sessionID來取得session ID。可以在後端用console log來觀察這些變數:

app.get('/', (req, res) => {
console.log(req.session)
console.log(req.sessionID)
})

撰寫登入邏輯

首先,先寫一段處理登入的程式碼,為求簡單,這邊把帳號密碼直接寫在server,要注意一般來說是會存在資料庫的:

app.post('/login', (req, res) => {
const users = [
{
firstName: 'Tony',
email: '[email protected]',
password: 'iamironman'
},
{
firstName: 'Steve',
email: '[email protected]',
password: 'icandothisallday'
},
//...後略
]

const { email, password } = req.body

if (email.trim() === '' || password.trim() === '') {
return res.render('index', { alert: 'Password or email is incorrect, please try again!' })
}

for (let user of users) {
if (user.email === email && user.password === password) {
req.session.user = user.firstName
return res.redirect('/welcome')
}
}

return res.render('index', { alert: 'Password or email is incorrect, please try again!' })

})

重新看一下驗證的流程:

使用者登入 -> 驗證資料庫內的帳號密碼 -> 通過後在伺服器端把使用者資訊存入session store,並生成session ID作為索引-> 給使用者session ID存放在瀏覽器的cookie -> 下次使用者進入頁面時,比對cookie內的session ID和伺服器端一不一樣 -> 如果一樣則回傳使用者資訊,不一樣則不讓使用者進入頁面。

程式碼粗體的部分就是在做「把使用者資訊存入session store」,這邊是使用req.session.user = user.firstName來把使用者名字存在session store。

當存入後,因為session被修改過了,express-session就會幫我們把帶有session ID 的session內容存入store,並把session ID簽名後存放在使用者瀏覽器cookie。

打開瀏覽器developer tool即可看到一組key-value pair在cookie裡面,粗體表示session ID:

user: s%3Ah30wc0pvSHJCYLVz0tui0Cv6NCCexGV8.Cw7O3d354j%2FMcrxrXuWb65%2FRUpV6QdV07axdG0rZc8Y

假設我以Tony登入,這時候req.session印出來會長這樣,可以看到已經把user: ‘Tony’寫進session內容了:

Session {
cookie: {
path: '/',
_expires: null,
originalMaxAge: null
httpOnly: true
},
user: 'Tony'
}

req.sessionID則是這樣:

h30wc0pvSHJCYLVz0tui0Cv6NCCexGV8

比對一下,server端存的req.sessionID的確和cookie存放的session ID是符合的,這邊先不贅述express-session怎麼幫session ID做簽名並存放在cookie的,有興趣可以看這篇。

撰寫驗證登入狀態邏輯

接著再來寫驗證登入狀態的部分:

function auth(req, res, next) {
if (req.session.user) {
console.log('authenticated')
next()
} else {
console.log('not authenticated')
return res.redirect('/')
}
}

app.get('/welcome', auth, (req, res) => {
const userName = req.session.user
return res.render('welcome', { message: `Welcome back, ${userName}!`
})
})

由於現在每次使用者的request都帶有session ID(來自瀏覽器cookie),當使用者送出request時,他的session ID就是對應到伺服器session store的{session ID: session內容}。所以當我們寫req.session時,express-session就會知道要去session store找這個使用者的session ID 相對應的session內容。

這邊auth middleware 做的事就是去檢查req.session.user是否存在,如果存在才能夠繼續進入/welcome路由。可以去developer tool改改看coookie的session ID,更改後因為session store沒有相應的ID,所以req.session.user會是undefined,進而無法進入此路由。

另外,只要登入過後,伺服器就只認session ID,所以如果有人取得了cookie儲存的session ID,他就可以使用你的帳號,這就叫做session hijacking 。

登出

登出可以使用express-session給的destroy方法來清理session store:

app.get('/logout', auth, (req, res) => {
req.session.destroy(() => {
console.log('session destroyed')
})

res.render('index', { alert: 'You are logged out! Re-enter email and password to log in again!' })

})

以上,又花了好多時間寫一篇文,心得跟每篇文一樣,每次越寫越覺得自己不懂,寫完總有種撥雲見日的感覺XD 網路上其實解釋express-session怎麼實做的文章都蠻零碎的,希望這篇文有達到統整資訊的目的!

推薦閱讀

大力推薦這系列的三篇文,應該全中文社群沒有辦法再找到寫得更詳細的cookie-session解釋了:

其他參考資料: