Hôm nay mình sẽ hướng dẫn các bạn xây dựng API để đăng ký đăng nhập với nodejs, express, mongodb và jwt.

Xây dựng api login register với nodejs xác thực Với JWT 2021

2021-07-24 3226 lượt xem

JWT Là Gì?

JSON Web Token (JWT) là một chuẩn mở (RFC 7519) định nghĩa một cách nhỏ gọn và khép kín để truyền thông tin một cách an toàn giữa các bên dưới dạng đối tượng JSON.

Tóm lại ngắn gọn sẽ là jwt là cái Auth0 để login 😄 chỉ thế thôi.

Cài cắm nodejs npm thì chắc chắn bạn phải biết rồi, nếu bạn nào chứ biết start 1 nodejs nhanh với express thì xem thử qua bài này : Tạo 1 app nodejs đầu tiên sử dụng Express application generator 2021. Bạn nào còn chưa biết tạo kết nối đến mongodb thông qua mongoose thì xem bài này : Kết nối đến MongoDB với Nodejs dùng mongoose

Cài đặt packagejsonwebtoken

Vì chủ đề hôm nay là chúng ta cần làm việc với jwt nên việc cài đặt package cho việc giải mã và tạo ra jwt thông qua 1 thư viện là điều cần thiết

npm i jsonwebtoken

Để validate mình dùng cài 1 package có kiểu viết na ná jquery để dễ maintain chứ không dùng validate của nodejs hay dùng: 

npm i node-input-validator

Để băm password thì mình phải dùng 1 hàm băm và thường nodejs sẽ dùng : 

npm i bcrypt

Mình có tạo 1 file .env nên sẽ cần package dotenv

npm i dotenv

Tạo thư mụccontroller ngang cấp với app.js

Chúng ta cần tạo controller để làm việc với api. Mình hay tổ chức thư mục kiểu: 

|- PROJECT/
|     |- controller/
|     |-    |- Api/
|     |-    |-    |- user.controller.js
|     |- middleware/
|     |-    |- general.middleware.js
|     |-    |- jwt.middleware.js
|     |-    |- user.middleware.js
|     |- helper/
|     |-    |- auth.helper.js 
|     |-    |- error.helper.js
|     |- models/
|     |-    |- user.model.js
|     |- routes/
|     |-    |- api.js
|     |- .env
|     |- app.js
|     |- package.json

File auth.helper.js Dùng để định nghĩa phương thức xác tạo jwt và xác thực jwt trong mỗi request. Code file auth.helper.js  như sau: 

require('dotenv').config()
const jwt    = require('jsonwebtoken')
const secret = process.env.JWT_SECRET || 'jsonwebtoken-secret' // mình có dùng dotenv 
// vào file .env tạo 1 chuỗi JWT_SECRET bất kỳ
let hashTokenAccess = async user => {

    return jwt.sign(user, secret, { expiresIn: '1800s' })
}
module.exports = {
    hashTokenAccess
}

error.helper.js

module.exports.apiResponseErrorResource = function( req, res ){
    let response = {}
    if(req.errors){
        let errors = Object.keys(req.errors).map( objKey => {
            let error = req.errors[objKey];
            return { error: objKey, ...error }
        })
        response.code             = 422,
        response.message          = "đã có lỗi xảy ra"
        response.internal_message = "đã có lỗi xảy ra"
        response.errors           = errors
        return res.status(response.code).json(response)
    }
}

user.model.js: 

'use strict'

const bcrypt           = require('bcrypt'),
      SALT_WORK_FACTOR = 12,
      mongoose         = require('mongoose'),
      Schema           = mongoose.Schema
      
const UserSchema = new Schema(
    {
        username: {
            type: String,
            required: true,
            minlength: 1,
            maxlength: 3000
        },
        email: {
            type: String,
            required: true,
            unique: true,
            lowercase: true,
            trim: true,
            minlength: 1,
            maxlength: 500
        },
        password: {
            type: String,
            required: true,
            maxlength: 100000,
        }
    }, {
        timestamps: true
    }
)

/**
 * Là function để so sánh chuỗi hash password đã gửi vào có đúng với trên hệ thống không? 
 * @param { String } _password là password đang được hash 
 * @version 0.0.1
 */
UserSchema.methods.comparePassword = function(_password) {
    return bcrypt.compareSync(_password, this.password);
}

/**
 * Là function để trước khi lưu mới hoặc update mới password thì sẽ modify nó trước khi lưu
 * @async
 * @version 0.0.1
 */
UserSchema.pre('save', async function(next) {
    if (!this.isModified('password')) return next()
    /// nếu là thêm mới hoặc update password thì băm trước
    try {
        const salt    = await bcrypt.genSalt(SALT_WORK_FACTOR)
        this.password = await bcrypt.hash(this.password, salt)
        return next()
    } catch (err) {
        return next(err)
    }
})
UserSchema.methods.toResources = function() {   
    return {
        _id      : this._id,
        username : this.username,
        email    : this.email,
        createdAt: this.createdAt,
        updatedAt: this.updatedAt,
    }
}
module.exports = mongoose.model("users", UserSchema)
 

file user.controller.js như sau: 

const User       = require("../../models/user.model"),
      authHelper = require("../../helpers/auth.helper")

/**
 * Là function với method post để đăng ký 1 user vào hệ thống
 * @async
 * @method POST
 * @param {{ body: Object }} req 
 * @param { Object } res 
 * @returns { Promise<json> } là 1 json object trả về đối tượng user vừa đăng ký
 * @version 0.0.1
 * @see https://... đường đẫn đến swagger
 */
module.exports.register = async ( req, res ) => {

    let code     = 500,
        response = {}
    /// giả sử khúc này  tới đây bạn đã sử dụng middleware ở ngoài để validate dữ liệu đầu vào
    try {
        const { username, email, password } = req.body
        /// check email tồn tại
        const isExist = await User.findOne({ email })
        if( isExist ){
            code = 409 /// 409 Conflict
            throw new Error("email đã tồn tại!!")
        }
        /// lưu vào db mongo
        const user = await new User({ username, email, password }).save()
        /// khúc này nếu bạn kỹ tính hãy tạo 1 phương thức chung để format dữ liệu 
        /// còn mình làm nhanh thì trả ra dữ liệu luôn
        response.code             = 200
        response.data             = user.toResources()
        response.message          = "tạo user thành công"
        response.internal_message = `Bạn đã tạo thành công mới 1 user với email là ${email}`

        return res.status(response.code).json(response)

    } catch (error) {
        
        let err                       = { error: 'error', message: error.message }
            response.code             = code || 500
            response.message          = error.message
            response.internal_message = error.message
            response.errors           = [ err ]

        return res.status(response.code).json(response)
    }
}

/**
 * Là function với method post để login 1 user vào hệ thống
 * @async
 * @method POST
 * @param {{ body: Object }} req 
 * @param { Object } res 
 * @returns { Promise< JSON > } là 1 json object trả về jwt
 * @version 0.0.1
 * @see https://... đường đẫn đến swagger
 */
 module.exports.login = async ( req, res ) => {

    let code     = 500,
        response = {}
    /// giả sử khúc này  tới đây bạn đã sử dụng middleware ở ngoài để validate dữ liệu đầu vào
    try {
        const { email, password } = req.body
        /// check email tồn tại
        const user = await User.findOne({ email }) /// vì email duy nhất nên findOne
        if( !user ){
            code = 401 /// 401 Unauthorized
            throw new Error("Email hoặc password không đúng!!")
        }
        /// nếu có email cần check xem password có match đúng với db không? 
        const isMatch = await user.comparePassword(password)
        if( !isMatch ){
            /// nếu password không chính xác thì trả ra lỗi
            code = 401 /// 401 Unauthorized
            throw new Error("Email hoặc password không đúng!!")
        }
        /// nếu email và password chính xác thì tạo 1 mã jwt quăng ra cho ngừoi dùng
        const strJWT = await authHelper.hashTokenAccess(user.toResources())
        
        /// khúc này nếu bạn kỹ tính hãy tạo 1 phương thức chung để format dữ liệu 
        /// còn mình làm nhanh thì trả ra dữ liệu luôn
        response.code             = 200
        response.data             = strJWT
        response.message          = "User login thành công"
        response.internal_message = `Bạn đã login thành công mới 1 user với email là ${email}`

        return res.status(response.code).json(response)

    } catch (error) {
        
        let err                       = { error: 'error', message: error.message }
            response.code             = code || 500
            response.message          = error.message
            response.internal_message = error.message
            response.errors           = [ err ]
            
        return res.status(response.code).json(response)
    }
} 
module.exports.getUser = async (req, res) => {

    let response = {},
        code = 500
    try {
        const { user } = req
        /// response 
        response.code             = 200
        response.data             = user
        response.message          = "buộc phải login nè"
        response.internal_message = "buộc phải login nè"
        return res.status(response.code).json(response)

    } catch (error) {

        let err                       = { error: 'error', message: error.message }
            response.code             = code || 500
            response.message          = error.message
            response.internal_message = error.message
            response.errors           = [err]
        return res.status(response.code).json(response)
    }
}
//

Trước khi chúng ta define các router thì chúng ta nhìn qua vài middleware mình có dùng đến : 

general.middleware.js

let setAllowOrigin = (req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*")
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
    console.log("================================================")
    console.log( Date.now() + 'Request URL:', req.originalUrl)
    console.log("================================================")
    next()
}
let formatJsonApi = ( req, res, next ) => {
    res.setHeader('Content-Type', 'application/json')
    next()
}
module.exports = {
    formatJsonApi,
    setAllowOrigin,
}

jwt.middleware.js

require('dotenv').config()
const jwt    = require('jsonwebtoken')
const secret = process.env.JWT_SECRET || 'jsonwebtoken-secret' // mình có dùng dotenv 

let isAuth = async (req, res, next) => {

    let code     = 401,
        response = {}
    // Lấy token được gửi lên từ phía client, thông thường tốt nhất là các bạn nên truyền token vào header
    const access =  req.headers["x-access-token"] || req.headers["authorization"] || req.query.token || req.body.token
    try {

        if (!access) {
            code = 403
            /// không tồn tại access token
            throw new Error('Unauthorized!!!')
        }
        // Thực hiện giải mã token xem có hợp lệ hay không?
        // let user = await jwt.decode( access, secret ) => hàm này chỉ decode thôi nghen không phải xác thực
        const user = await jwt.verify( access, secret ) // hàm này để xác thực

        // Nếu token hợp lệ, lưu thông tin giải mã được vào đối tượng req, dùng cho các xử lý ở phía sau.
        req.user = user
        
        // Cho phép req đi tiếp sang controller.
        next();
    } catch (error) {
        
        // Nếu giải mã gặp lỗi: Không đúng, hết hạn...etc:
        response.code             = code || 401
        response.message          = error.message || 'Unauthorized.'
        response.internal_message = error.message || 'Unauthorized!'

        return res.status(response.code).json(response)
    }
}

let isAuthSocket = async token => {

    try {

        return await jwt.verify( token, secret )
    } catch (error) {
        return false
    }
}

module.exports = {
    isAuth,
    isAuthSocket,
}

user.middleware.js

const node_validator = require('node-input-validator'),
      errorHelper    = require('../helpers/error.helper'),
      { Validator }  = node_validator

let REGISTER = async function( req, res, next ){

    let validate = new Validator(req.body, {
        username: "required|string|minLength:1|maxLength:10000",
        email   : "required|email|minLength:3|maxLength:500",
        password: "required|string|minLength:1|maxLength:1000",
    },{
        'title.required'     : ":attribute is required"
    });
     
    let matched = await validate.check()
    if (!matched) {
        req.errors = validate.errors
        return errorHelper.apiResponseErrorResource( req, res )
    }
    next()
}

let LOGIN = async function( req, res, next ){

    let validate = new Validator(req.body, {
        email   : "required|email|minLength:3|maxLength:500",
        password: "required|string|minLength:1|maxLength:1000",
    },{
        'title.required'     : ":attribute is required"
    });
     
    let matched = await validate.check()
    if (!matched) {
        req.errors = validate.errors
        return errorHelper.apiResponseErrorResource( req, res )
    }
    next()
}

module.exports = {
    REGISTER,
    LOGIN,
}

Cuối cùng file routers/api.js sẽ như sau : 

/**
 * author hungtt
*/
const express = require("express")
const router  = express.Router()

const userApiController = require("../controller/Api/user.controller")
const generalMiddleware = require('../middlewares/general.middleware'),
      authMiddleware    = require('../middlewares/jwt.middleware'),
      userMiddleware    = require('../middlewares/user.middleware')
/**
 * Init all APIs on your application
 * @param {*} app from express
 */
let initAPIs = app => {
    ////////////////////////////////////////////////////////////////////////////
    router.use([ generalMiddleware.formatJsonApi, generalMiddleware.setAllowOrigin ])
    ////////////////////////////////////////////////////////////////////////////
    /////////////////// Route không cần login ////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////
    router.post('/register', [ userMiddleware.REGISTER ], userApiController.register)
    router.post('/login', [ userMiddleware.LOGIN ], userApiController.login)
    ////////////////////////////////////////////////////////////////////////////
    router.use([ authMiddleware.isAuth])
    ////////////////////////////////////////////////////////////////////////////
    /////////// route cần verify thành công jwt ////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////
    router.get('/users', userApiController.getUser )
    
    return app.use( "/api", router )
}
module.exports = initAPIs

Trong file app.js chỉ cần thực hiện thêm thao tác : 

const initAPIs = require("./routes/api")
/// set root api
initAPIs(app)

Xong ! bây giừo chạy npm start lên và xem kết quả: 

 

 

những tag
bài viết trong chủ đề