[專案紀錄] 記帳 web app 升級!實作登入功能 — (二) 登入/登出篇

I caught a code
I Caught a Code
Published in
8 min readJul 11, 2021

利用 Passport.js 打造登入驗證機制

以鳴人為主題的記帳本 web app

沒想到這系列的上一篇已經是兩個月前的事囉 🙃 趁這次回來重構優化的機會撿起來繼續講 XD

Live Demo on Heroku (先註冊一個帳號就能使用囉)

前一篇說明了註冊功能背後的流程,這篇來到登入/登出的部分,在這個專案裡我使用了 express-session 與 passport.js 來實作 cookie-based 的登入驗證機制。先來複習一下使用者認證的流程:

使用者認證

取自 Alpha Camp 教案

client 端發送登入的 request,伺服器端收到後,驗證該使用者資訊是否存在於資料庫中,有的話就將使用者資訊存入 session,並在 res.headers 中派發 session id 回去給 client 端並儲存於瀏覽器的 cookie 中。

之後當同一個 client 發送 request 時,只要附上同一組 session id 伺服器就能夠判斷這個 request 是來自一個登入過的使用者,可以使用授權的內容。

而登出就是消滅這組 session id,結束這次的 session。下次要再使用該網站時,就必須再重新登入、開啟新 session 、產生一組新的 session id。

若將 session id 赤裸裸地存在瀏覽器 cookie 中勢必不安全,會有可竄改或被冒用導致資訊外洩的疑慮,因此必須使用第三方套件來加密與保存。

passport.js

接著使用 passport.js 的 LocalStrategy 來實作本地端的登入驗證:

passport 套件本身內建許多驗證模組,而在這裡我透過比對 client 端資訊與資料庫中的使用者資料,來判定這個 request 是否通過使用者驗證。這邊也在判斷的邏輯中加入了 connect-flash ,來將驗證失敗的警示訊息傳送到前端。

而後半段的部分則是 passport 儲存與解析資訊的方式:

透過序列化來把 user.id 存入 session 中,在需要存取整包 user 物件時再透過反序列化將資料解析出來,這麼做可以節省空間,存 user.id 比存一大包資料還要更輕量化。

引自 Alpha Camp 教案

express-session

通過驗證拿到使用者資訊後,我們要開啟新的 session ,以及派發並儲存 session id。這邊使用的是 express-session,它會將 session id 與一組用來防止資訊被篡改的「簽章 (signature)」(session id + 只有伺服器端知道的 secret)組合加密後回傳給 client 讓它儲存於 cookie 中,方便之後發送 request 給伺服器時,能透過 request 夾帶的 session id 來辨別狀態。

我們在 app.js 中載入 express-session 套件作為 middleware 使用,設定包含了一組只有我們自己知道的 secret:

// app.js
const session = require('express-session')
...(略)app.use(session({
secret: 'ThisIsMySecret', // 隨意取,須妥善保存不可外洩
resave: false,
saveUninitialized: true
}))

express-session + passport.js 組合技

再來就到 app.js 中載入 passport 模組:

// app.js
const session = require('express-session')
const usePassport = require('./config/passport')
...(略)usePassport(app) // 要寫在路由之前~必須先通過驗證才能使用其他路由模組

回到路由模組(已經分流過、與 app.js 拆開)中,到跟使用者資源相關的 users.js 中引入 passport 的 middleware:

// routes/modules/users.js
const passport = require('passport')
router.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/users/login'
}))
// 使用 local strategy, 驗證通過就跳轉到首頁,失敗就跳轉到登入頁面

到這邊登入驗證的部分完成啦。

而登出就相對簡單,passport 有內建登出的函式 req.logout(),會直接幫我們清除 session:

// routes/modules/users.js
...(略)
router.get('/logout', (req, res) => {
req.logout()
res.redirect('/users/login')
})

登出登出都完成了,那麼我們在一次 session 中不可能只待在同一頁,一定會使用到不同頁面的內容,要怎麼告訴伺服器我們是有登入的狀態呢?

passport 內建函式作為 middleware

我們可以使用 passport 內建的 req.isAuthenticated() 函式,來作為使用每條路由之前,幫助我們確認登入狀態的 middleware authenticator

// middleware/auth.js
module.exports = {
authenticator: (req, res, next) => {
if (req.isAuthenticated()) {
return next()
}
res.redirect('/users/login')
}
}

再到總路由器 routes/index.js 中把這個 middleware 加入到需要驗證登入狀態的路由中:

// routes/index.js
const express = require('express')
const router = express.Router()
const { authenticator } = require('../middleware/auth')
const home = require('./modules/home')
const records = require('./modules/records')
const users = require('./modules/users')
const auth = require('./modules/auth')
const analysis = require('./modules/analysis')
router.use('/records', authenticator, records)
router.use('/analysis', authenticator, analysis)
router.use('/users', users) // 登入登出的頁面不用加
router.use('/auth', auth) // 後來加的 fb/google 驗證頁面也不需要
router.use('/', authenticator, home)
module.exports = router

這邊 users 的路由模組不用加,是因為我們前面將登入與登出頁面的路由寫在裡面,而這兩條路由是任何人都能夠使用的,不需驗證狀態。

到這裡就完成登入、登出、與保存狀態的機制囉!是不是很簡單呢><

passport 真的是很方便的套件,一些社群或購物網站常見的 facebook 登入、google 登入,都可以用 passport 的不同 strategy 來實作(最近在寫的小型電商網站就使用了 facebook 與 google 登入),官方文件也相對平易近人,對初學者來說比較好下手!

後記

本來兩個月前打算寫個系列文,從註冊登入機制寫到資料關聯,寫了兩篇之後就遇到爆肝的 twitter 協作專案,一直到結業後都還是忘記這個主題還在…

過了這麼久,手頭上還有好幾個 side project 在進行,一一討論實作細節的文章看來只能在登入機制這邊收尾了XD

下次就寫重構心得吧~(可以想見又是一篇感慨成長的文 🚬

--

--