「使用passport middleware實作登入系統」相關筆記
2021-06-24 13:41 Express
總結
本次練習包含以下幾個主要功能:
- 使用passport來驗證使用者輸入的登入資訊(帳號、密碼)是否有效
- 並
passport.authenticate()
會接手處理驗證成功/失敗的 redirect endpoint - 搭配
req.isAuthenticated()
設計 middleware 來控制登入前後可視之畫面(例:登入前不可查看/dashboard 頁面、登入後無法查看/user/login 頁面)
- 並
- 使用connect-flash搭配 express-handlebars 的 partial 來實現
res.redirect()
後顯示錯誤訊息的功能
環境
passport: 0.4.1
passport-local: 1.0.0
express: 4.17.1
express-session: 1.17.2
connect-flash: 0.1.1
os: Windows_NT 10.0.18363 win32 x64
筆記
app.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const express = require('express') | |
const app = express() | |
const port = 5000 | |
// DB | |
require('./config/mongoose') | |
// form handling | |
const bodyParser = require('body-parser') | |
app.use(bodyParser.json()) | |
app.use(bodyParser.urlencoded({ extended: true })) | |
// session setting for passport | |
const session = require('express-session') | |
app.use(session({ | |
secret: 'keyboard cat', | |
resave: true, | |
saveUninitialized: true | |
})) | |
// display message after redirect by session and flash | |
const flash = require('connect-flash') | |
app.use(flash()) | |
app.use((req, res, next) => { | |
res.locals.successMessage = req.flash('successMessage') | |
res.locals.errorMessage = req.flash('errorMessage') | |
res.locals.error = req.flash('error') | |
next() | |
}) | |
// passport | |
const passport = require('passport') | |
const loginVerify = require('./config/passport') | |
loginVerify(passport) | |
app.use(passport.initialize()) | |
app.use(passport.session()) | |
// rendering template | |
const expressHandlebars = require('express-handlebars') | |
app.engine('handlebars', expressHandlebars({ defaultLayout: 'main' })) | |
app.set('view engine', 'handlebars') | |
// routes | |
const routes = require('./routes') | |
app.use(routes) | |
// scripts, styles | |
app.use(express.static('public')) | |
app.listen(port, () => { | |
console.log(`Express is listening on localhost:${port}`) | |
}) |
- 13 行開始:參考passport 的官方示範,將
resave
與saveUninitialized
兩個參數都設定為true
- 根據express-session 的官方文件:
resave
: Forces the session to be saved back to the session store, even if the session was never modified during the request.saveUninitialized
: Forces a session that is “uninitialized” to be saved to the store. A session is uninitialized when it is new but not modified.
- 21 行開始
res.locals.successMessage = req.flash('successMessage')
: Every view will have access to any error or success messages that you flash. Ref.: How to send flash messages in Express 4.0?res.locals
: An object that contains response local variables scoped to the request, and therefore available only to the view(s) rendered during that request / response cycle (if any). Otherwise, this property is identical toapp.locals
. Ref.: Express: res.locals- 須注意
req.flash('error')
此名稱為 passport 專用,將 error 修改為其他名稱後,passportdone()
的錯誤訊息就無法顯示
config/passport.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const LocalStrategy = require('passport-local').Strategy | |
const bcrypt = require('bcrypt') | |
const User = require('../models/user') | |
function loginVerify (passport) { | |
passport.use(new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => { | |
const user = await User.findOne({ email }) | |
// check if user exist | |
if (!user) return done(null, false, { message: 'User not exist' }) | |
// check password | |
const compareResult = await bcrypt.compare(password, user.password) | |
return compareResult ? done(null, user) : done(null, false, { message: 'Password incorrect' }) | |
}) | |
) | |
passport.serializeUser((user, done) => { | |
done(null, user.id) | |
}) | |
passport.deserializeUser((id, done) => { | |
User.findById(id, (error, user) => { | |
done(error, user) | |
}) | |
}) | |
} | |
module.exports = loginVerify |
- 說明:
function loginVerify(passport)
匯出後由app.js
require 後使用,參考app.js
原始碼 33-34 行 - 第 6 行的
{ usernameField: 'email' }
- By default, LocalStrategy expects to find credentials in parameters named
username
andpassword
. If your site prefers to name these fields differently, options are available to change the defaults. - 登入表單中的 username 與 password name 欄位若不是
name="username"
與name="password"
的話,可額外指定要讀取的表格欄位,{ usernameField: 'email' }
的意思即是使用登入表單中的name="email"
欄位作為usernameField
- By default, LocalStrategy expects to find credentials in parameters named
- 第 9 行的
{ message: 'User not exist' }
:其中'User not exist'
會由req.flash('error')
顯示 done(null, user)
與done(null, false)
done(null, user)
: If the credentials are valid, the verify callback invokesdone()
to supply Passport with the user that authenticated.done(null, false)
: If the credentials are not valid,done()
should be invoked with false instead of a user to indicate an authentication failure.
serializeUser
與deserializeUser
- 16 行:Only the
user.id
is serialized to the session, keeping the amount of data stored within the session small. - 20 行:When subsequent requests are received, this ID (
user.id
) is used to find the user, which will be restored toreq.user
. - Ref.: Understanding passport serialize deserialize
Q: Where does
user.id
go afterpassport.serializeUser
has been called? A: Theuser.id
is saved in the session, and is later used to retrieve the whole object via thedeserializeUser
function.serializeUser
determines which data of the user object should be stored in the session.
- 16 行:Only the
config/auto.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function isLoggedIn (req, res, next) { | |
if (req.isAuthenticated()) { | |
return next() | |
} | |
req.flash('errorMessage', 'Please log in to view this page.') | |
res.redirect('/user/login') | |
} | |
function notLoggedIn (req, res, next) { | |
if (!req.isAuthenticated()) { | |
return next() | |
} | |
res.redirect('/dashboard') | |
} | |
module.exports = { isLoggedIn, notLoggedIn } |
-
關於
.isAuthenticated()
- How is req.isAuthenticated() in Passport JS implemented? For any request, you can check if a user is authenticated or not by using this method.
- No Mention of isAuthenticated() in docs #683
There is no bug… The thing is that the
isAuthenticated()
andisUnauthenticated()
functions are not mentioned anywhere in the docs.
-
在
routes/home.js
與routes/user.js
中作為 middleware 使用isLoggedIn
:若使用者已登入,則可繼續前往該 endpoint;反之若使用者未登入的話,則導向/user/login
,並顯示提示訊息(Please log in to view this page.
)notLoggedIn
:若使用者在已經登入的情況下前往/user/login
或/user/register
,則自動導回/dashboard
routes/modules
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const express = require('express') | |
const router = express.Router() | |
// check if user has logged in | |
const { isLoggedIn, notLoggedIn } = require('../../config/auth') | |
router.get('/', notLoggedIn, (req, res) => { | |
res.render('index') | |
}) | |
router.get('/dashboard', isLoggedIn, (req, res) => { | |
res.render('dashboard', { user: req.user.username }) | |
}) | |
module.exports = router |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const express = require('express') | |
const router = express.Router() | |
// DB | |
const User = require('../../models/user') | |
// password hash | |
const bcrypt = require('bcrypt') | |
const saltRounds = 10 | |
// passport | |
const passport = require('passport') | |
// only direct not logged in user to login or register endpoint | |
const { notLoggedIn } = require('../../config/auth') | |
router.get('/login', notLoggedIn, (req, res) => { | |
res.render('login') | |
}) | |
router.post('/login', passport.authenticate('local', { | |
successRedirect: '/dashboard', | |
failureRedirect: '/user/login', | |
failureFlash: true | |
})) | |
router.get('/register', notLoggedIn, (req, res) => { | |
res.render('register') | |
}) | |
router.post('/register', async (req, res) => { | |
const { username, email, password } = req.body | |
const registerErrors = [] | |
if (!username || !email || !password) registerErrors.push({ message: 'Please fill in all fields' }) | |
if (email) { | |
const find = await User.findOne({ email }) | |
if (find) registerErrors.push({ message: 'User already exist' }) | |
} | |
if (registerErrors.length) { | |
res.render('register', { registerErrors, username, email, password }) | |
return | |
} | |
const hashPassword = await bcrypt.hash(password, saltRounds) | |
const newUser = new User({ | |
username, | |
email, | |
password: hashPassword | |
}) | |
await newUser.save() | |
req.flash('successMessage', 'You are now register and can log in.') | |
res.redirect('/user/login') | |
}) | |
router.get('/logout', (req, res) => { | |
req.logout() | |
req.flash('successMessage', 'You have logged out.') | |
res.redirect('/user/login') | |
}) | |
module.exports = router |
- home.js
- 第 12 行
res.render('dashboard', { user: req.user.username })
:通過登入驗證的使用者資料會被儲存在req.user
中,要在dashboard
模板中顯示username
的話,從req.user
中取username
的值即可
- 第 12 行
- user.js
- 21-25 行:直接由
passport.authenticate('local', { ... })
接管登入驗證的程序,驗證成功的話導向/dashboard
,失敗則導回/user/login
,並因為failureFlash
設定為true
,login
模板會根據 config/passport.js 中設定的條件顯示相對應的錯誤訊息({ message: 'User not exist' }
與{ message: 'Password incorrect' }
) - Setting the
failureFlash
option totrue
instructs Passport to flash an error message using the message given by the strategy’s verify callback, if any. 需注意 views/partials 中的{{#if error}}
不可換成其他名稱,req.flash('error')
中的'error'
也不可換(換了就無法顯示訊息) req.logout()
: Passport exposes alogout()
function onreq
that can be called from any route handler which needs to terminate a login session. Invokinglogout()
will remove thereq.user
property and clear the login session (if any).
- 21-25 行:直接由
views/partials
- 使用 partials 時的 views 資料夾結構:
/views
/layouts
main.handlebars
/partials
messages.handlebars
dashboard.handlebars
index.handlebars
login.handlebars
register.handlebars
- 使用方式:設定好 partials 內容後,將
{{> 檔案名稱}}
(例:{{> messages}}
)插回其餘模板即可 - 會自動根據
{{#if 參數是否為true}}
來決定顯示哪些 partials 內容,舉例:若 passport.js 中的done()
回傳 error 的話,partials 檔案messages.handlebars
中的 24-29 行即會顯示在login
或register
模板中