本文為 Lidemy [BE201] > [超重要觀念:Middleware] 之 [做一個簡單會員註冊系統] 來學習後端 express 的實作過程,以下摘要內含:實作目標、資料夾結構、基本流程、MVC 功能簡介、使用 bcrypt middleware 加密密碼
零、內容摘要
1. 目標是要做出三個頁面,分別是首頁、註冊及登入,可以寫入且讀取資料庫
2. 資料夾結構
controllers
- user.js: 一個個函式
models
- user.js:跟之前寫 sql query 類似
views
user folder
login.ejs:登入頁面的畫面
register.ejs:註冊頁面的畫面
index.ejs:畫面的首頁
db.js:帳號、密碼,負責連線
index.js:負責引入 library 及路由
3. 基本流程:開 table → 寫 models → 寫 views → index.js 寫路由 → 寫 controllers
註:每寫一小段就跑一下看看
4. models: 以前 sql query 的寫法,select or insert into…
5. views: html tag + <%= js 想輸出的東西 %>
6. controllers: 包著一個個函式:
a. 讀取的(.get()
)就只渲染畫面
login: (req, res) => {
res.render('user/login')
},
b. 寫入的(.post()
)裡面拿資料並且處理錯誤
handleLogin: (req, res, next) => {
const { username, password, nickname } = req.body
if (!username || !password) {
req.flash('errorMessage', 'Please fill in all the required fields.')
return next()
}
userModel.get(username, (err, user) => {
if (err) {
req.flash('errorMessage', err.toString())
return next()
}
if (!user) {
req.flash('errorMessage', 'invalid user')
return next()
}
bcrypt.compare(password, user.password, function(err, isSuccess) {
if (err || !isSuccess) {
req.flash('errorMessage', 'invalid password')
return next()
}
req.session.username = user.username
res.redirect('/')
});
})
},handleLogin: (req, res, next) => {
const { username, password, nickname } = req.body
if (!username || !password) {
req.flash('errorMessage', 'Please fill in all the required fields.')
return next()
}
userModel.get(username, (err, user) => {
if (err) {
req.flash('errorMessage', err.toString())
return next()
}
if (!user) {
req.flash('errorMessage', 'invalid user')
return next()
}
bcrypt.compare(password, user.password, function(err, isSuccess) {
if (err || !isSuccess) {
req.flash('errorMessage', 'invalid password')
return next()
}
req.session.username = user.username
res.redirect('/')
});
})
},
7. 用了 bcrypt 這個 middleware 來加密密碼
一、基本設置
1. sequel pro 開一個叫 users 的 table,包含:
id, username, password, nickname
2. 先寫 model
models > user.js
const db = require('../db')
const userModel = {
// 傳一個 user 裡面包含所有資料
add: (user, cb) => {
db.query(
'insert into users(username, password, nickname) values(?, ?, ?)',
[user.username, user.password, user.nickname],
(err, results) => {
if (err) return cb(err)
cb(null)
});
},
get: (username, cb) => {
db.query(
'SELECT * from users where username = ?', [username],
(err, results) => {
if (err) return cb(err)
cb(null, results[0])
});
}
}
module.exports = userModel
3. 把剛剛 index.js 裡面的 controller 整理一下
// index.js
app.get('/login', userController.login)
app.post('/login', userController.handleLogin)
app.get('/logout', userController.logout)
4. 把剛剛 index.js 裡面的函式放過來 controllers > user
// controllers > user.js
const userModel = require('../models/user')
const userController = {
get: (req, res) => {
},
login: (req, res) => {
res.render('login')
},
handleLogin: (req, res) => {
if (req.body.password === 'abc') {
req.session.isLogin = true
res.redirect('/')
} else {
req.flash('errorMessage', 'invalid password')
res.redirect('/login')
}
},
logout: (req, res) => {
req.session.isLogin = false
res.redirect('/')
}
}
module.exports = userController
5. views > user.js
// index.ejs
<h1>簡易會員系統</h1>
<% if(isLogin) { %>
Hello, user!
<a href="logout">logout</a>
<% } else { %>
Wanna login?
<a href="/register">register</a>
<a href="/login">login</a>
<% } %>
要記得在 index.js 渲染畫面才會出現喔
// index.js
app.get('/', (req, res) => {
res.render('index')
})
二、實作註冊頁面
1. index.js 加上路由
// index.js
app.get('/register', userController.register)
app.post('/register', userController.handleRegister)
2. controllers
error log 1: Error: Route.post() requires a callback function but got a [object Undefined]
錯誤描述:localhost:5001 無法跑起來
解法一:先把這行註解 app.post('/register', userController.handleRegister)
,估計是因為 controller 裡面的 handleRegister 沒有寫好
後來發現是拼錯字啦,吼唷
error log 2: Error: Connection lost: The server closed the connection.
[Nodejs] 解決MySQL Error: Connection lost. The server closed the connection的方法:這個還沒解決,之後再嘗試吧
// controller > user.js
const res = require('express/lib/response')
const userModel = require('../models/user')
const userController = {
get: (req, res) => {
},
login: (req, res) => {
res.render('login')
},
handleLogin: (req, res) => {
if (req.body.password === 'abc') {
req.session.isLogin = true
res.redirect('/')
} else {
req.flash('errorMessage', 'invalid password')
res.redirect('/login')
}
},
register: (req, res) => {
res.render('user/register')
},
handleRegister: (req, res) => {
const { username, password, nickname } = req.body
if (!username || !password || !nickname) {
return req.flash('errorMessage', 'Please fill in all the required fields.')
}
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
// Store hash in your password DB.
});
userModel.add({
username,
password,
nickname
}, (err) => {
if (err) {
return req.flash('errorMessage', err.toString())
}
req.session.username = username
res.redirect('/')
})
},
logout: (req, res) => {
req.session.username = null
res.redirect('/')
}
}
module.exports = userController
3. views 註冊頁面
// views > register.ejs
<h1>register</h1>
<h2><%= errorMessage %></h2>
<form method="POST" action="/register">
<div>username: <input type="text" name="username" /></div>
<div>nickname: <input type="text" name="nickname" /></div>
<div>password: <input type="password" name="password" /></div>
<input type="submit" />
</form>
4. 密碼加密
a. 參考 node.bcrypt.js 安裝 $ npm install bcrypt
b. 注意這裡要在 controllers > user.js 裡面引入
// controllers > user.js
const bcrypt = require('bcrypt')
const saltRounds = 10
c. 下面程式碼放在 controller > user.js 的 handleRegister 拿到密碼之後
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
// Store hash in your password DB.
});
handleRegister: (req, res) => {
const { username, password, nickname } = req.body
if (!username || !password || !nickname) {
return req.flash('errorMessage', 'Please fill in all the required fields.')
}
bcrypt.hash(password, saltRounds, function(err, hash) {
if (err) {
return req.flash('errorMessage', err.toString())
}
userModel.add({
username,
password: hash,
nickname
}, (err) => {
if (err) {
return req.flash('errorMessage', err.toString())
}
req.session.username = username
res.redirect('/')
})
});
},
三、實作登入頁面
1. 從路由開始
// index.js
app.get('/login', userController.login)
app.post('/login', userController.handleLogin, redirectBack)
app.get('/logout', userController.logout)
2. controllers 就會呼叫相對應的 models
// controllers > user.js
const userModel = require('../models/user')
const bcrypt = require('bcrypt')
const saltRounds = 10
const userController = {
login: (req, res) => {
res.render('login')
},
handleLogin: (req, res, next) => {
const { username, password, nickname } = req.body
if (!username || !password) {
req.flash('errorMessage', 'Please fill in all the required fields.')
return next()
}
userModel.get(username, (err, user) => {
if (err) {
req.flash('errorMessage', err.toString())
return next()
}
if (!user) {
req.flash('errorMessage', 'invalid user')
return next()
}
bcrypt.compare(password, user.password, function(err, isSuccess) {
if (err || !isSuccess) {
req.flash('errorMessage', 'invalid password')
return next()
}
req.session.username = user.username
res.redirect('/')
});
})
},
register: (req, res) => {
res.render('user/register')
},
handleRegister: (req, res, next) => {
const { username, password, nickname } = req.body
if (!username || !password || !nickname) {
req.flash('errorMessage', 'Please fill in all the required fields.')
return next()
}
bcrypt.hash(password, saltRounds, function(err, hash) {
if (err) {
req.flash('errorMessage', 'user exists')
return next()
}
userModel.add({
username,
password: hash,
nickname
}, (err) => {
if (err) {
req.flash('errorMessage', 'user exists')
return next()
}
req.session.username = username
res.redirect('/')
})
});
},
logout: (req, res) => {
req.session.username = null
res.redirect('/')
}
}
module.exports = userController
3. model 會是這樣
// models > user.js
const db = require('../db')
const userModel = {
add: (user, cb) => {
db.query(
'insert into users(username, password, nickname) values(?, ?, ?)',
[user.username, user.password, user.nickname],
(err, results) => {
if (err) return cb(err)
cb(null)
}
);
},
get: (username, cb) => {
db.query(
'SELECT * from users where username = ?', [username],
(err, results) => {
if (err) return cb(err)
cb(null, results[0])
}
);
}
}
module.exports = userModel
4. template engine 會自動防止 xss、sql injection 攻擊
xss:記得要用的是
<%= username %>
,如果是減號<%- username %>
就不會防止攻擊,輸入什麼就是什麼sql injection:用 ? 代替輸入,
'SELECT * from users where username = ?', [username],