Hướng Dẫn Xây Dựng NodeJS API Xác Thực Với JWT

Hướng Dẫn Xây Dựng NodeJS API Xác Thực Với JWT

Giới Thiệu Chung

Hôm nay mình sẽ hướng dẫn các bạn xây dựng API xác thực với nodejs, express và mongodb.
API xác thực này sẽ bao gồm các chức năng đăng ký, đăng nhập, validate đầu vào, hash pass, xác thực với jwt,..

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. Token bao gồm một header, một payload và một chữ ký.
Các bạn có thể tìm hiểu thêm về JWT tại đây.

JWT Hoạt Động Như Thế Nào?

Thì mình mới sưu tầm được trên google một hình vẽ khá chi tiết về cách hoạt động của JWT.

Ở đây mình cũng giải thích một tí cho các bạn hiểu thêm về cách hoạt động của JWT, đó là khi chúng ta đăng nhập một tài khoản và gửi các thông tin như email/password lên server thì nó sẽ ký một token và gửi token đó về phía client để lưu trữ.
Cứ như vậy mỗi lần client truy cập trang thì sẽ gửi request lên thì phải kèm theo token, sau đó server sẽ check mã này và gửi lại response thành công hay thất bại tương ứng ngược về client.

Mục Đích Bài Viết

Giúp các bạn ôn lại các kiến thức về NodeJS, cách xây dựng cũng như truy vấn database MongoDB.
Và học cũng như tìm hiểu thêm về JWT, cách xây dựng một API về xác thực và cách sử dụng postman.
Sau bài này các bạn có thể tự mình làm một trang web gồm các chức năng đăng nhập, đăng ký từ API mình xây dựng được. Đó cũng là một cách giúp các bạn có thể nắm vững các kiến thức được học.
Mình mong muốn các bạn đọc bài một cách tỉ mỉ, đọc không lướt nha.

Bắt Đầu Thôi Nào

Lên Ý Tưởng Xây Dựng API Xác Thực

Trước khi các bạn làm một cái gì đó thì trước hết phải hình thành ý tưởng, sau đó triển khai ý tưởng. Điều này giúp các bạn làm việc một cách có logic và khoa học hơn.
Trước khi chúng ta bắt tay vào code thì mình sẽ giúp các bạn nắm rõ hơn ý tưởng mình cần làm nha.
Chúng ta sẽ xây dựng các chức năng đăng nhập và đăng ký trước tiên đã. Đăng nhập và đăng ký thì chúng ta cần phải validate cho nó, mã hóa password,..Sử dụng JWT để tạo các private routes.
Ở đây mình chỉ nói sơ qua thôi để cho các bạn có thể hình dung ra được mình cần làm gì thôi nha. Các bạn cũng theo dõi các phần tiếp theo để biết thêm chi tiết nha.

Cài Đặt Và Thiết Lập

Trước tiên các bạn tạo cho mình một folder trong folder đó là nơi chứa các thư mục và chương trình mà chúng ta dùng để viết API xác thực.
Khởi tạo ứng dụng với file package.json
Trong thư mục gốc của ứng dụng của bạn và nhập npm init để khởi tạo ứng dụng của bạn với tệp package.json.
npm init
Sau đó các bạn cài đặt các module ở dưới để thiết lập ứng dụng nha.
Để mà cài các module trước hết các bạn phải cài đặt NodeJS tại đây.

  • npm install express --save
    Module dùng để cài đặt framework express của nodejs giúp các bạn code một cách tối giản cũng như nhanh chóng hơn.
  • npm install body-parse --save
    Package body-parser dùng để truy xuất dữ liệu trong form gửi lên server theo phương thức POST.
  • npm install nodemon --save
    Tự động reload lại server khi bạn thay đổi code. Để khởi chạy server các bạn thêm "devStart": "nodemon app.js" vào file package.json nha. Các bạn mở terminal lên sau đó gõ npm run devStart để khởi chạy server nha.
  • npm install mongoose --save
    Mongoose là một Object Document Mapper (ODM). Điều này có nghĩa là Mongoose cho phép bạn định nghĩa các object (đối tượng) với một schema được định nghĩa rõ ràng.
  • npm install dotenv --save
    Dotenv là một biến môi trường dùng để bảo mật các thông tin quan trọng như username, password, url database,...Nếu chúng ta không lưu những thông tin mật vào file .env thì khi push source code lên github thì ai cũng có thể vào xem được và ai cũng biết username, password thì sẽ bị người khác chiếm đoạt và đánh cắp tài liệu rất là nguy hiểm nha.
  • npm install @hapi/joi --save
    Module Joi này dùng để xác nhận dữ liệu, ví dụ như bạn khi bạn nhập bạn nhập password bắt buộc phải nhập trên 6 từ hay là email các bạn phải nhập đúng các kiểu kí tự để tạo email,..thì các bạn cần phải xác nhận cho nó bằng module joi.
  • npm install bcryptjs --save
    Module dùng để mã hóa password.
  • npm install jsonwebtoken --save
    Module này dùng để xác thực và tạo các private routes.
    Và đây cũng là package của mình khi đã cài đặt cũng như cấu hình xong các npm cho ứng dụng.

Trong thư mục ứng dụng của chúng ta bây giờ hiện có ba file đó là node_modules, package.jsonpackage-lock.json

Authentication with JWT
     └── node_modules
     └── package-lock.json
     └── package.json

Bắt Đầu Code Thôi Nào

Thiết Lập Web Server

Trong thư mục gốc các bạn tạo cho mình file app.js, file này là file chính dùng để điều khiển mọi hoạt động chính của server.

const express = require("express");
const app = express();
const bodyParser = require('body-parser');
const port = 3333;

// Import routes
const indexUser = require("./routes/index")

// Gửi yêu cầu phân tích kiểu nội dung application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }))

// Gửi yêu cầu phân tích kiểu nội dung application/json
app.use(bodyParser.json())

// Route middlewares
app.use('/api/user',indexUser)

// Lắng nghe các requests
app.listen(port, function(){
    console.log("Server listening port",+port)
})

Trong thư mục gốc bạn tạo cho mình thư mục routes, trong thư mục router các bạn tạo cho mình file index.js. Tiếp đến tạo cho nó một Route handlers và thử chạy sever xem sau. Mục đích để các bạn test xem server có chạy không nha.

router.get('/',function(req, res, next){
res.send('Hello World')
})

Thiết Lập Database

Để mà có thể thiết lập được database thì các bạn đăng nhập tài khoản mongodb atlas tại đây nha. Sau khi các bạn bạn đã có tài khoản mongodb atlas thì tiếp đến cài đặt cấu hình để có thể kết nối mongodb atlas với ứng dụng của chúng ta. Để biết thêm chi tiết các bạn xem bước 3 trong bài viết của mình hướng dẫn deploy project nodejs lên heroku tại đây nha.
Sau đó các bạn copy link database mongodb đã tạo rồi paste vào project của mình là được nha. Các bạn tạo cho mình một file .env bên trong thư mục gốc.
Trong file này các bạn tạo một đường dẫn là DATABASE_URL=mongodb+srv://long:<password>@cluster0-b7d8c.mongodb.net/test?retryWrites=true&w=majority
Trong file app.js các bạn khai báo module dotenv với module mongoose và tạo đường dẫn mongodb.
// Khai báo dotenv
require('dotenv').config()
// path database
var mongoose = require('mongoose');

// Kết nối database
mongoose.connect(process.env.DATABASE_URL, 
{useNewUrlParser: true, useUnifiedTopology: true}).then(function() {
    console.log("Successfully connected to the database");    
}).catch(function(err) {
    console.log('Could not connect to the database. Exiting now...', err);
    process.exit();
});

Để biết nó đã kết nối hay chưa các bạn khởi động lại server sẽ biết nha.

Định Nghĩa Các Schema User Cho Mongoose

Bên trong thư mục gốc các bạn tạo cho mình thư mục modal bên trong thư mục modal các bạn tạo cho mình file user.modal.js.
Trong file user.modal.js cần tạo một đối tượng book và sẽ chứa ba thuộc tính là name, email và password.

const mongoose = require("mongoose")

const userSchema = mongoose.Schema({
    name:{required: true, type:String},
    email:{required: true, type:String},
    password:{required: true, type:String},
})
// Biên dịch mô hình từ schema
module.exports = mongoose.model('user', userSchema)

Tham số thứ nhất là tên riêng cho collection sắp được tạo ra cho mô hình của bạn, và tham số thứ hai là schema mà bạn muốn dùng để tạo ra mô hình.
Sau khi chúng ta đã setup xong hệ thống thì bây giờ mình sẽ đi đến từng chức năng cụ thể nha. Thì chức năng đầu tiên mình muốn giới thiệu đến là chức năng đăng ký.

Xử Lý Yêu Cầu Đăng Ký

Trong phần xử lý yêu cầu đăng ký chúng ta sẽ chia làm 3 phần:
Phần 1: Tạo user.
Phần 2: Validate user.
Phần 3: Check email có tồn tại hay không và mã hóa password.
Chúng ta sẽ bắt tay vào làm từng phần luôn nha.

Tạo User

Trong file index.js nằm trong thư mục routes, các bạn khai báo cho mình modal user lúc nãy mình vừa tạo.
const User = require("../modal/user.modal")
Thứ hai, các bạn tạo cho mình router Post /register. Trong router này chúng ta sử dụng để tạo user cũng như validate user, hash pass,...
Nó sẽ tạo một object rộng, chứa các dữ liệu user được truyền vào. Rồi lưu dữ liệu vào database đã được khởi tạo.
Chúng ta lưu dữ liệu cũng như log ra lỗi trong try{} catch{} nha. Nếu mà chúng ta nhập đầy đủ dữ liệu thì nó sẽ thực hiện trong hàm try rồi sau đó log ra dữ liệu của user.
Nếu bạn không nhập đầy đủ dữ liệu cũng như có lỗi xảy ra thì nó sẽ thực hiện hàm catch để log ra lỗi, các bạn có thể test thử trong postman để có thể hiểu rõ hơn.

router.post('/register', async function(req, res){

     // Tạo user
     const newuser = new User();
     newuser.name = req.body.name
     newuser.email = req.body.email
     newuser.password = req.body.password
     try{
         const User = await newuser.save()
         res.send(User);
     }catch(err){
         res.status(400).send(err);
     }
})

Chúng ta cùng test API này bằng postman để tạo user thử nha.

Validate User

Sau khi chúng ta tạo user xong thì sẽ dến phần xác nhận user. Xác nhận ở đây là chúng ta xác nhận đầu vào của user. Ví dụ như: bạn nhập có đúng email không, password phải dài hơn 6 kí tự,...
Thì để xác thực như vậy chúng ta sử dụng module Joi nha. Module này mình đã cài đặt ở trên rồi đó.
Trong thư mục gốc các bạn tạo cho mình folder có tên là auth trong thư mục đố các bạn tiếp tục tạo cho mình file validation.js.
File validation.js sẽ là file mà chúng ta dùng để điều khiển mọi hoạt động chính liên quan đến xác nhận.
Trước tiên các bạn khai báo cho mình module Joi. Các bạn có thể tham khảo thêm về joi tại đây
Data ở đây là tham số chúng ta truyền vào dùng để xác nhận và đó là các trường dữ liệu như: name, email, password.

const Joi = require('@hapi/joi');

// Register Validate
const registerValidation = function(data){
    const schema = Joi.object ({
        name: Joi.string()
                 .min(4)
                 .required(),
        email: Joi.string()
                   .email()
                   .min(6)
                   .required(),
        password: Joi.string()
                   .min(6)
                   .required(),
                          
    })
   return  schema.validate(data)
}
module.exports.registerValidation = registerValidation

Để mà nó có thể hoạt động được chúng ta cần phải import registerValidation từ file validation.js vào trong file index.js của routes.
const {registerValidation} = require("../auth/validation")
Cũng trong router Post /register các bạn code thêm một đoạn mã nữa ở trên thằng tạo user để có thể xác nhận đầu vào cho nó.

 // Validate user
    const{ error } = registerValidation(req.body);
     if(error) return res.status(400).send(error.details[0].message)

Data được truyền vào đó là req.body, nếu gặp lỗi thì log ra thông báo.
Chúng ta cùng xem thử nó xác nhận đầu vào như thế nào nhé
Chúng ta để ý trong password mình chỉ viết 3 kí tự thì nó liền log ra thông báo ngay, chúng ta đã yêu cầu là password phải 6 kí tự trở lên mới đúng.

Check Email Có Tồn Tại Hay Không Và Mã Hóa Password

Khi chúng ta đăng ký một tài khoản thì email là phần quan trọng nhất mỗi tài khoản sẽ có một email riêng. Name và password có thể giống nhau nhưng email là không bao giờ giống nhau được.
Vì thế chúng ta cần check email user để xem email họ đăng ký có trùng với email đã đăng ký hay chưa.
Cũng trong router Post /register các bạn code thêm một đoạn mã ở dưới thằng xác nhận user để có thể check email có tồn tại hay không.
Nó sẽ tìm trong database xem thử thằng email mà mình nhập nó có trùng với email nào trong database hay không. Nếu có thì sẽ log ra thông báo.

     // Kiểm tra email có tồn tại hay không
     const emailExist = await User.findOne({email: req.body.email});
     if(emailExist) return res.status(400).send("Email đã tồn tại")

Chúng ta xem thử nó kiểm tra email có tồn tại hay không như thế nào nhé
Bây giờ chúng ta thử đăng ký email trùng với email mà mình đã tạo rồi và xem thử kết quả nha.

Mã Hóa Password

Mã hóa password là phần rất là quan trọng và không thể thiếu trong mỗi website khi tạo user. Mục đích chính là bảo mật người ví dụ như khi các hacker tấn công website và lấy được database thì họ sẽ không thể chiếm đoạt được tài khoản của chúng ta vì password đã được mã hóa.
Trong file index.js của routes các bạn khai báo module bcrypt để có thể hash pass nha.
const bcrypt = require("bcryptjs")
Cũng trong router Post /register các bạn code thêm một đoạn mã nữa ở phía dưới thằng check email tồn tại nha để có thể mã hóa password cho nó.
Với bcrypt ta cần chú ý tới rounds truyền vào để sinh salt. Với rounds càng lớn thì càng bảo mật nhưng thời gian xử lý cũng mất nhiều hơn.

     // Mã hóa password
     
     const salt = await bcrypt.genSalt(10);
     const hashPass = await bcrypt.hash(req.body.password, salt)// mã hóa password truyền vào và gán nó vào biến hashPass để có thể lưu pass vào database.

Trong phần tạo user các bạn thay đổi một chút nha. Thay đổi newuser.password = req.body.password thành newuser.password = hashPass nha để nó có thể lưu pass đã thay đổi vào database.
Chúng ta cùng xem thử nó mã hóa ra sao nhé

Vậy là chúng ta đã xong phần đăng ký tiếp theo là đến phần đăng nhập nha.

Xử Lý Yêu Cầu Đăng Nhập

Trong phần đăng nhập mình chia làm 2 phần.
Phần 1: Validate user.
Phần 2: Check email và password.
Chúng ta sẽ bắt tay vào làm từng phần luôn nha.

Validate User

Thì thằng này cũng tương như thằng xác nhận user của đăng ký thôi, chỉ khác ở chổ từ ba trường bây giờ mình chỉ xác nhận 2 trường thôi.
Cũng trong file validation.js các bạn thêm cho mình phần xác nhận user để xử lý đăng nhập và exports nó ra là được.

// Login Validate
const loginValidation = function(data){
    const schema = Joi.object ({
        email: Joi.string()
                   .email()
                   .min(6)
                   .required(),
        password: Joi.string()
                   .min(6)
                   .required(),
    })
   return  schema.validate(data)
}
module.exports.loginValidation = loginValidation

Để mà nó có thể hoạt động được chúng ta cần phải import loginValidation từ file validation.js vào trong phần khai báo trước đó của chức năng đăng ký.
const {registerValidation, loginValidation} = require("../auth/validation")
Các bạn tạo cho router Post/login trong router này chúng ta sẽ sử dụng để xác nhận user, kiểm tra email và password.
Data được truyền vào đó là req.body, nếu gặp lỗi thì log ra thông báo.

router.post('/login', async function(req, res){
    // Validate user
    const{ error } = loginValidation(req.body);
     if(error) return res.status(400).send(error.details[0].message)
})

Chúng ta cùng xem thử nó xác nhận đầu vào khi login như thế nào nhé
Chúng ta để ý trong password mình chỉ viết 3 kí tự thì nó liền log ra thông báo ngay, chúng ta đã yêu cầu là password phải 6 kí tự trở lên mới đúng.

Check email và password

Để có thể đăng nhập được thì chúng ta cần phải kiểm tra email và password mình đã được tạo hay chưa và đăng nhập thông tin có chính xác không.
Cũng trong router Post /register các bạn code thêm một đoạn mã ở dưới thằng xác nhận user để có thể check email và password có chính xác hay không.
Trong phần kiểm tra email các bạn sẽ tìm trong database có email nào trùng với email mình nhập không, khi chúng ta sử dụng findOne thì nó sẽ log ra toàn dữ liệu của user có email trùng với email mình nhập. Nếu email nhập sai thì nó sẽ log ra thông báo.
Trong phần kiểm tra password thì chúng ta sẽ nhập password đăng nhập vào và mã hóa password đó đi, sau đó kiểm tra password đã được mã hóa có trùng với password cũng đã được mã hóa và lưu trong database chưa. Nếu password nhập sai thì nó sẽ log ra thông báo.
Còn nếu tất cả các trường hợp trên từ validate đến check email và password đều chính xác thì sẽ log ra thông báo "Bạn đã đăng nhập thành công".

     // Kiểm tra email
    const userLogin = await User.findOne({email: req.body.email});
    if(!userLogin) return res.status(400).send("Không tìm thấy email")

     // Kiểm tra password
    const passLogin = await bcrypt.compare(req.body.password, userLogin.password);
    if(!passLogin) return res.status(400).send("Mật khẩu không hợp lệ")

     res.send("Bạn đã đăng nhập thành công")

Chúng ta cùng xem thử nếu chúng ta đăng nhập chính xác thì nó sẽ hiển thị như thế nào nhé, còn các trường hợp khác các bạn tự test nha.

Xử Lý Xác Thực Bằng JWT

JWT là gì và cách hoạt động của nó như nào mình đã nói ở phần mở đầu rồi, giờ mình không nói lại nữa mà bắt tay vào làm luôn nha.
Trong phần xử lý xác thực bằng jwt chúng ta sẽ chia làm 3 phần:
Phần 1: Tạo Token.
Phần 2: Verify Token.

Tạo Token

Để có thể tạo được token các bạn cần phải khai báo module JWT mà chúng ta đã cài đặt ở trên rồi ấy.
const jwt = require("jsonwebtoken")
Vậy bây giờ làm thế nào để có thể tạo và assign token. Trong router Post/login các bạn thay thế cho mình thông báo này res.send("Bạn đã đăng nhập thành công") khi đăng nhập thành công. Bằng một đoạn code dùng để tạo mã JWT và gửi về cho user.
Trong file .env các bạn tạo cho mình mã secretkey(SECRET_TOKEN), mã này phải bảo mật tuyệt đối nên mình phải lưu nó và biến môi trường.
Sau đó chúng ta có thể add nó vào header với bất kì loại định danh nào mà chúng ta mong muốn. Nói đơn giản là token được tạo sẽ được lưu vào headers.

    // Ký và tạo token
   const token = jwt.sign({_id: userLogin._id}, process.env.SECRET_TOKEN)
   res.header("auth-token", token).send(token);

Chúng ta cùng xem thử nó tạo token như thế nào nhé.

Sau khi các bạn tạo token xong bây giờ các bạn cần phải verify token.

Verify Token

Trong thư mục auth các bạn tạo cho mình thêm file checkToken.js trong file này các bạn sẽ code cho mình một đoạn mã dùng để verify Token.

const jwt = require("jsonwebtoken")

module.exports = function(req, res, next){
   const token = req.header('auth-token'); // sẽ gửi yêu cầu lên header để tìm                                                   token ra rồi verify. 
   // Nếu bạn không truyền lên token thì nó sẽ gửi thông báo 
   if(!token) return res.status(401).send("Vui lòng đăng nhập để được truy cập")
   try{
        const checkToken = jwt.verify(token, process.env.SECRET_TOKEN) // kiểm tra token
       req.user = checkToken //lưu token lại để có thể kiểm tra 
       next()
   }catch(err){
       res.status(400).send('Token không hợp lệ')// thông báo lỗi khi bạn nhập sai token.
   }
}

Để có thể sử dụng được các bạn cần phải import file checkToken.js vào trong file index.js.
const verify = require("../auth/checkToken")
Các bạn tạo cho mình một router Get / để có thể tạo cho nó một middleware dùng để xác thực token có hợp lệ không.

router.get('/', verify, function(req, res){
    res.send("Chào mừng bạn đến với website của mình. Chúc bạn một ngày vui vẻ")
})

Chúng ta thử lấy token đã tạo ở trên để gọi API Get /
Cái tên mà mình gạch chân màu đỏ ấy các bạn phải nhập chính xác như trong code nha

Chúng ta cùng test thêm vài trường hợp không truyền mã token hoặc gửi sai mã token xem nó như thế nào nhé

Vậy là xong rồi nha, các bạn có thể tham khảo code mà mình đã push lên github tại đây nha

Lời Kết

Vậy Là Xong bài Hướng Dẫn Xây Dựng NodeJS API Xác Thực Với JWT rồi nhé. Mình mong muốn sau bài topic này các bạn có thể nắm vững thêm về các kiến thức NodeJS, Express, MongoDB và Authentication, JWT,... Và có thể tự tay mình làm những project không cần phải quá đặc biệt nhưng nó do chính bạn làm thì cũng coi như là thành quả trong quá trình bạn học được.

Nếu các bạn cảm thấy bài viết của mình hay thì các bạn có thể ủng hộ mình để mình có thêm động lực để ra những bài topic hay và chất lượng hơn ủng hộ mình tại đây nha.

Chúc Các Bạn Thành Công!!