環境簡介
1 2 3 4 5 6 7 8
| - controllers - auth.js - model - User.js - route - auth.js - middleware - authentication
|
這次要製作註冊登入功能,route 和 controllers 資料夾理會有 auth的 路由(route) 和 函式(controller),另外有建立User的model。本次使用 express搭配 mongoDB製作。
建立User的schema
首先在 model裡的 User.js建立user的 schema,也就是每筆使用者的資料格式,包括用戶姓名、email、password等等。會用到mongoose提供的驗證方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const UserSchema = new mongoose.Schema({ name: { type: String, required: [true, 'Please provide name'], maxlength: 50, minlength: 3, }, email: { type: String, required: [true, 'Please provide email'], match: [ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 'Please provide a valid email', ], unique: true, }, password: { type: String, required: [true, 'Please provide password'], minlength: 6, }, })
|
在user的schema裡可以新增加密的方法,使用 bcrypt套件來對password加密,因為希望password加密後再存入資料庫。
1 2 3 4
| UserSchema.pre('save', async function () { const salt = await bcrypt.genSalt(10) this.password = await bcrypt.hash(this.password, salt) })
|
註冊路由
註冊使用者的 controller 函式如下:
1 2 3 4 5 6 7 8 9 10
| const User = require('../models/User') const { StatusCodes } = require('http-status-codes') const { BadRequestError, UnauthenticatedError } = require('../errors')
const register = async (req, res) => { const user = await User.create({ ...req.body }) const token = user.createJWT() res.status(StatusCodes.CREATED).json({ user: { name: user.name }, token }) }
|
製作註冊功能時,使用者在前端會傳入 name 跟 password 的資料,資料會以物件方式存在 req.body裡
1 2 3 4
| { name: '王曉明', password: '123456' }
|
當路由收到資料後,用 .create()
建立新的使用者,此時如果使用少傳了name或password,會觸發 mongoose 的驗證機制,直接拋出錯誤。
建立使用者後,用寫在User的schema上的方法來產生 jwt的 token,最後這個token會回傳給前端做下次登入驗證用,詳細jwt用法看這裡
1 2 3 4 5 6 7 8 9
| UserSchema.methods.createJWT = function () { return jwt.sign( { userId: this._id, name: this.name }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_LIFETIME, } ) }
|
登入路由
當使用者成功註冊後,可以進行登入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const login = async (req, res) => { const { email, password } = req.body
if (!email || !password) { throw new BadRequestError('Please provide email and password') } const user = await User.findOne({ email }) if (!user) { throw new UnauthenticatedError('Invalid Credentials') } const isPasswordCorrect = await user.comparePassword(password) if (!isPasswordCorrect) { throw new UnauthenticatedError('Invalid Credentials') } const token = user.createJWT() res.status(StatusCodes.OK).json({ user: { name: user.name }, token }) }
|
首先從req.body裡可以取得前端傳來的 email和password資料
先檢查 email 和 password 是否存在,不存在的話直接丟出錯誤。
確認有帳密資料後,用 email資料取得資料庫裡對應的user資料,如果找不到user則回傳錯誤。
再來比對使用者輸入的密碼是否一樣,使用 bcrypt套件裡的compare功能,比對的函式一樣放在UserSchema的methods裡。
1 2 3 4
| UserSchema.methods.comparePassword = async function (canditatePassword) { const isMatch = await bcrypt.compare(canditatePassword, this.password) return isMatch }
|
確認密碼一樣後,就可以新增 jwt token 並回傳端,登入成功。
新增驗證的middleware
使用者註冊和登入後,前端會獲得token,作為之後驗證使用。可以在middleware資料夾內新增 autentication的js檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const auth = async (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer')) { throw new UnauthenticatedError('Authentication invalid') } const token = authHeader.split(' ')[1]; try { const payload = jwt.verify(token, process.env.JWT_SECRET); req.user = { userId: payload.userId, name: payload.name }; next(); } catch(err) { throw new UnauthenticatedError('Authentication invalid') } }
|
在驗證邏輯中,前端會將token放在headers.authorization裡,所以先判斷 req 裡有沒有 authorization 的資料,沒有則回報錯誤。
若有則將token字串取出,之後用
1
| jwt.verify(token, token金鑰)
|
將token資料解析回當初加密前的物件格式
1 2 3 4
| { name: 'user', userId: '123456' }
|
取得物件後,就將資訊存在 req.user裡,方便之後再路由裡取得user資訊。