初探 Middleware
在 Express 這個框架裡面 Middleware 扮演著中繼站的角色,當 Server 收到 request 之後,到回傳 response 的這段時間,會經過一系列的 Middleware,比如說一個登入的動作可能就包含負責不同功能(輸入帳號,身份驗證,傳輸資料,顯示畫面)的 Middleware 在其中。
每個 Middleware 會有自己負責的功用,比如說我們在上一篇文章提到的 Controller 就是一種 Middleware。
通常 Middleware 都會有一個標準的形式:
(req, res, next) => {
...
}
傳入的參數:
- req:代表 request
- res:代表 response
- next:用來表示是否要將 req 跟 res 的控制權傳給下一個 Middleware
我們以一個權限管理的功能舉例:
- 我們希望使用者一點開網站就做驗證
- 驗證通過,才會把 req 跟 res 的控制權傳給下一個 Middleware
function checkPermission(req, res, next) {
if (<驗證條件>) { // 如果驗證條件通過
next(); // 才會把 req 跟 res 的控制權傳給下一個 Middleware
} else {
res.end('Error'); // 否則就顯示 Error
}
}
app.use( checkPermission ) // 把 function 傳入 .use()
.use()
這個函式,代表這個 Middleware 影響的範圍會是整個 app- 如果希望影響的只有其中一個路由,可以把
checkPermission
加在那個路由底下
這樣寫 checkPermission 的這個 Middleware 影響的範圍就只有 './todos' 這個路由而已
app.get('./todos', checkPermission, todoController.getAll)
app.get('./todos/:id', todoController.get)
- 所以 ./todos 這個路由被觸發的時候,會先執行 checkPermission 這個 Middleware,等到結束後它會把全控制權傳給下一個 todoController.getAll Middleware
常用 Middleware
☞ body-parser
功用:express 本身只能在 URL 上面拿資料(用 req.query 語法),但是 POST 方法會把資料放在 request.body 裡面,所以要利用 body-parser
這個 Middleware 來幫助我們取得夾帶在 body 裡面的資料
安裝:
npm install body-parser
使用方法:
const bodyParser = require('body-parser') // 引入 body-parser
// 設定要 Parse 的資料格式是 urlencodes 還是 json,通常兩個都會用到,所以都引入進來
app.use(bodyParser.urlencoded({ extended: false })) // 一般在瀏覽器 POST 的資料格式
app.use(bodyParser.json()) // json 格式的資料
加上上面兩個設定之後,就可以實際用 req.body.<name>
去取得 body 裡面的資料了
瀏覽器端:
<form method="post" action="todos">
<input type="text" name="content" />
<input type="submit" />
</form>
伺服器端:
app.post('./todos', (req, res, next) => {
const content = req.body.content // 利用 req.body.<name> 去取得 body 裡面的資
})
☞ Express-Session
功用:處理 Session 相關的功能
安裝:
npm install express-session
使用方法:
const session = require('express-session')
app.set('trust proxy', 1)
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
// cookie: { secure: true }
}))
加入上面這些設定之後,就可以實際用 req.session.<參數>
來存取 session 了
app.post('./login', (req, res, next) => {
if (username === '...' && password === '...' ) {
req.session.username = username; // 設定 session.username
res.redirect('back') // 導回上個頁面
} else {
res.redirect('/login')
}
})
在 render 的時候就可以判斷登入狀態決定要顯示的畫面內容
<% if (req.session.username) { %> // 如果 session.username 有值的話
<h1>你已經登入</h1>
<% } else { %>
<h1>你還沒登入</h1>
<% } %>
☞ connect-flash
功用: 可以存放你想使用的訊息,例如:輸入密碼錯誤、帳號已被註冊,之類的提示訊息。先將這些訊息存放在 flash 當中,等到需要顯示這些提示訊息的時候,就把 flash 裡的顯示顯示於畫面上,一但脫離這個畫面,或者重新整理頁面,訊息就會自動消失。
安裝:
npm install connect-flash
使用方法:
const flash = require('connect-flash');
app.use(flash());
// 只要有設定 Express-Session 就不用做 connect-flash 的額外設定
加入上面這些設定之後,就可以實際用 req.flash(<參數>)
來存取 flash 了
- 使用方式跟 session 很類似
- flash() 如果帶入兩個參數:代表將數值存入 flash
req.flash('errorMassage', 'Please input the correct password')
- 將 Please input...... 存在 errorMassage 底下
- flash() 如果只帶一個參數:代表取出存在這個參數底下的 flash
req.flash('errorMassage')
- 取出存在 errorMassage 這個參數底下的 Please input...... 訊息
app.post('./login', (req, res, next) => {
if (username === '...' && password === '...' ) {
req.session.username = username;
res.redirect('back')
} else {
req.flash('errorMessage', 'Please input the correct password'); // 將 Please input...... 存在 errorMassage 底下
res.redirect('/login')
}
})
app.get('./login', (req, res, next) => {
res.render('login', {
errorMessage: req.flash('errorMessage'); // 取出存在 errorMassage 這個參數底下的 Please input...... 訊息
})
})
如此一來,就可以在 login 失敗的時後,加上顯示給使用者錯誤訊息
- 如果同一條錯誤訊息,很多地方需要用到,每一次 render 都把參數帶進去其實很麻煩
- 在 express 有一個叫做
res.locals
的地方,可以讓我們放我們想放入的訊息,並且這些訊息,可以在 view 裡面直接存取到
app.use((req, res, next) => {
res.locals.errorMessage = req.flash('errorMessage'); // 寫在 res.locals 底下的東西 view 裡面都能拿到
next() // 記得要 next 讓下一個 Middleware 可以拿到使用權
})
<% if (req.session.username) { %> // 如果登入成功
<h1>你已經登入</h1>
<% } else { %>
<%= errorMessage %> // 在 view 裡面直接存取放在 res.locals 底下的訊息
<% } %>
所以 res.locals
就有點像一個 global 的地方,不同 view 都需要用到的訊息,統一放在這裡,所有的 view 就都能存取的到
Express 的 hash 方法
我們要引入一個 Library 叫做 bcrypt
安裝:
npm install bcrypt
使用方法:
const bcrypt = require('bcrypt');
const saltRounds = 10; // 設置 hash 的複雜度
bcrypt.hash(password, saltRounds, function(err, hash) {
// 程式碼: 決定 hash 過後的 password 要如何處理
});
☞ 儲存註冊的密碼:
bcrypt.hash()
需要傳入三個參數:
- 第一個參數:要做 hash 的變數 (也就是我們的 password)
- 第二個參數:saltRounds
- 第三個參數:call back function (決定 hash 過後的 password 要如何處理)
- 這個 call back function 裡面的第二個參數 hash 代表已經被 hash 過後的密碼
bcrypt.hash(password, saltRounds, function(err, hash) {
if (err) {
req.flash('errorMessage', err.toString());
return res.redirect('back');
}
userModel.add({ // 將註冊的帳號密碼傳給 Model
nickname,
username,
password: hash // 寫進去的密碼就會是 hash 過後的密碼,而不是原來的 password 明碼
})
})
☞ 匹配登入的密碼:
bcrypt.compare()
:需要傳入三個參數:
- 第一個參數:使用者輸入的明碼
- 第二個參數:資料庫存放的 hash 過後的密碼
- 第三個參數:call back function ( 把使用者輸入的密碼 hash 過後跟資料庫的密碼比對)
- 這個 call back function 裡面的第二個參數 result,當匹配結果成功的時候就會是 true
bcrypt.compare(password, hash, function(err, result) {
// 如果 result 為 true 代表匹配結果為正確
if (err || !res) {
req.flash('errorMessage', err.toString());
return res.redirect('back');
}
req.session.username = username
res.redirect('/')
});
Express 的 escape
Express 的 View 已經幫我們做好 escape 了
所以 <%= username %>
如果裡面有 HTML 的程式碼也沒有關係,所以就不用擔心 XSS 的問題。
除非使用 <%- username %>
當我們把符號換成 -
號, tamplate engine 就不會自動幫我們做 escape 了