nodejs-jwt-authenticate-user-trungquandev-image-feature

NodeJS xác thực người dùng sử dụng JWT (Access Token, Refresh Token)

Xin chào tất cả các bạn, mình là Quân, ở bài hôm trước, chúng ta đã cùng nhau đào khá sâu vào lý thyết của thằng JWT – JSON Web Tokens rồi, bài hôm nay chúng ta sẽ chỉ hoàn toàn là thực hành code thôi nhé.

“Bài này nằm trong loạt bài Lập Trình Nodejs từ cơ bản đến nâng cao trên trang blog chính thức trungquandev.com

Những nội dung có trong bài:

  1. Phân tích bài toán đặt ra
  2. Lao vào code
  3. Full source code trên Github

1. Phân tích bài toán đặt ra

Phần này đơn giản mình gạch ra vài đầu dòng cho tính năng như sau nha:

  • Khi người dùng call api login đăng nhập thành công, thực hiện tạo accessTokenrefreshToken gửi về cho client lưu trữ.
  • Với những api tiếp theo cần xác thực và bảo vệ, thì chúng ta sẽ yêu cầu người dùng truyền lên accessToken để phía server kiểm tra ok thì mới cho phép api đó hoạt động.
  • Khi accessToken hết hạn, sẽ sử dụng một api làm mới token, api này sử dụng refreshToken đã tạo ở bước login để làm mới accessToken

Tất cả nội dung lý thuyết chuyên sâu về JSON Web Token mình đã nói hết ở bài trước, bạn nào chưa đọc thì tham khảo ở đây trước khi bắt tay vào thực hành bài này nhé:
https://trungquandev.com/hieu-sau-ve-jwt-json-web-tokens/

(Ngoài lề: Mấy bạn admin của mấy trang TopDev, TechBlog…vv gì đấy đã đi copy bài không phải của các bạn thì hãy tôn trọng người viết, đừng có xóa những liên kết (link) trong bài của mình như link ở trên cũng như tự ý xóa linh tinh các câu thoại của mình trong bài viết, từ giờ nếu mình phát hiện ra nữa thì chắc chắn sẽ ăn report DMCA nhé.)


2. Lao vào code

Cấu trúc thư mục dự án của chúng ta sẽ trông như sau

src

controllers

AuthController.js

FriendController.js

helpers

jwt.helper.js

middleware

AuthMiddleware.js

routes

api.js

server.js

package.json

Việc khởi tạo ứng dụng nodejs thì mình không làm lại, các bạn có thể xem cách làm ở các bài viết trước của mình tại link bên dưới đây:
https://trungquandev.com/series-lap-trinh-nodejs/

Tiếp theo, trong ví dụ ngày hôm nay, chúng ta sẽ cài đặt 2 module là:

expressjsonwebtoken

npm install --save express jsonwebtoken

Mình sẽ hướng dẫn lần lượt các file code như sau:

File: src/helpers/jwt.helper.js

Trong file helper này mình sẽ sử dụng module jsonwebtoken để viết 2 chức năng là generateToken – tạo tokenverifyToken – xác minh token có hợp lệ hay không.

Ngoài ra lưu ý cái đoạn khai báo thuật toán algorithm: "HS256" lúc ký token, thuật toán này default là HS256, các bạn có thể không khai báo vào đây cũng được. Mình để trong code vì có thể nhiều bạn sẽ chọn các thuật toán khác như HS384, RS256…vv

/**
 * Created by trungquandev.com's author on 16/10/2019.
 * src/controllers/auth.js
 */
const jwt = require("jsonwebtoken");

/**
 * private function generateToken
 * @param user 
 * @param secretSignature 
 * @param tokenLife 
 */
let generateToken = (user, secretSignature, tokenLife) => {
  return new Promise((resolve, reject) => {
    // Định nghĩa những thông tin của user mà bạn muốn lưu vào token ở đây
    const userData = {
      _id: user._id,
      name: user.name,
      email: user.email,
    }
    // Thực hiện ký và tạo token
    jwt.sign(
      {data: userData},
      secretSignature,
      {
        algorithm: "HS256",
        expiresIn: tokenLife,
      },
      (error, token) => {
        if (error) {
          return reject(error);
        }
        resolve(token);
    });
  });
}

/**
 * This module used for verify jwt token
 * @param {*} token 
 * @param {*} secretKey 
 */
let verifyToken = (token, secretKey) => {
  return new Promise((resolve, reject) => {
    jwt.verify(token, secretKey, (error, decoded) => {
      if (error) {
        return reject(error);
      }
      resolve(decoded);
    });
  });
}

module.exports = {
  generateToken: generateToken,
  verifyToken: verifyToken,
};

File: src/middleware/AuthMiddleware.js

Kế tiếp, ở cái AuthMiddleware mình sẽ viết một middleware isAuth có chức năng bảo vệ những api cần bảo mật, một chút nữa ở file routes/api.js chúng ta sẽ dùng tới.

/**
 * Created by trungquandev.com's author on 16/10/2019.
 * src/controllers/auth.js
 */
const jwtHelper = require("../helpers/jwt.helper");
const debug = console.log.bind(console);

// Mã secretKey này phải được bảo mật tuyệt đối, các bạn có thể lưu vào biến môi trường hoặc file
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET || "access-token-secret-example-trungquandev.com-green-cat-a@";

/**
 * Middleware: Authorization user by Token
 * @param {*} req 
 * @param {*} res 
 * @param {*} next 
 */
let isAuth = async (req, res, next) => {
  // 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 tokenFromClient = req.body.token || req.query.token || req.headers["x-access-token"];

  if (tokenFromClient) {
    // Nếu tồn tại token
    try {
      // Thực hiện giải mã token xem có hợp lệ hay không?
      const decoded = await jwtHelper.verifyToken(tokenFromClient, accessTokenSecret);

      // 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.jwtDecoded = decoded;

      // 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:
      // Lưu ý trong dự án thực tế hãy bỏ dòng debug bên dưới, mình để đây để debug lỗi cho các bạn xem thôi
      debug("Error while verify token:", error);
      return res.status(401).json({
        message: 'Unauthorized.',
      });
    }
  } else {
    // Không tìm thấy token trong request
    return res.status(403).send({
      message: 'No token provided.',
    });
  }
}

module.exports = {
  isAuth: isAuth,
};

File: src/controllers/AuthController.js

File AuthController.js này sẽ bảo gồm 2 controller login – thực hiện chức năng đăng nhập, tạo token và controller refreshToken – làm mới lại token khi hết hạn.

/**
 * Created by trungquandev.com's author on 16/10/2019.
 * src/controllers/auth.js
 */
const jwtHelper = require("../helpers/jwt.helper");
const debug = console.log.bind(console);

// Biến cục bộ trên server này sẽ lưu trữ tạm danh sách token
// Trong dự án thực tế, nên lưu chỗ khác, có thể lưu vào Redis hoặc DB
let tokenList = {};

// Thời gian sống của token
const accessTokenLife = process.env.ACCESS_TOKEN_LIFE || "1h";
// Mã secretKey này phải được bảo mật tuyệt đối, các bạn có thể lưu vào biến môi trường hoặc file
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET || "access-token-secret-example-trungquandev.com-green-cat-a@";

// Thời gian sống của refreshToken
const refreshTokenLife = process.env.REFRESH_TOKEN_LIFE || "3650d";
// Mã secretKey này phải được bảo mật tuyệt đối, các bạn có thể lưu vào biến môi trường hoặc file
const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET || "refresh-token-secret-example-trungquandev.com-green-cat-a@";

/**
 * controller login
 * @param {*} req 
 * @param {*} res 
 */
let login = async (req, res) => {
  try {
    debug(`Đang giả lập hành động đăng nhập thành công với Email: ${req.body.email} và Password: ${req.body.password}`);
    // Mình sẽ comment mô tả lại một số bước khi làm thực tế cho các bạn như sau nhé:
    // - Đầu tiên Kiểm tra xem email người dùng đã tồn tại trong hệ thống hay chưa?
    // - Nếu chưa tồn tại thì reject: User not found.
    // - Nếu tồn tại user thì sẽ lấy password mà user truyền lên, băm ra và so sánh với mật khẩu của user lưu trong Database
    // - Nếu password sai thì reject: Password is incorrect.
    // - Nếu password đúng thì chúng ta bắt đầu thực hiện tạo mã JWT và gửi về cho người dùng.
    // Trong ví dụ demo này mình sẽ coi như tất cả các bước xác thực ở trên đều ok, mình chỉ xử lý phần JWT trở về sau thôi nhé:
    debug(`Thực hiện fake thông tin user...`);
    const userFakeData = {
      _id: "1234-5678-910JQK-tqd",
      name: "Trung Quân",
      email: req.body.email,
    };

    debug(`Thực hiện tạo mã Token, [thời gian sống 1 giờ.]`);
    const accessToken = await jwtHelper.generateToken(userFakeData, accessTokenSecret, accessTokenLife);
    
    debug(`Thực hiện tạo mã Refresh Token, [thời gian sống 10 năm] =))`);
    const refreshToken = await jwtHelper.generateToken(userFakeData, refreshTokenSecret, refreshTokenLife);

    // Lưu lại 2 mã access & Refresh token, với key chính là cái refreshToken để đảm bảo unique và không sợ hacker sửa đổi dữ liệu truyền lên.
    // lưu ý trong dự án thực tế, nên lưu chỗ khác, có thể lưu vào Redis hoặc DB
    tokenList[refreshToken] = {accessToken, refreshToken};
    
    debug(`Gửi Token và Refresh Token về cho client...`);
    return res.status(200).json({accessToken, refreshToken});
  } catch (error) {
    return res.status(500).json(error);
  }
}

/**
 * controller refreshToken
 * @param {*} req 
 * @param {*} res 
 */
let refreshToken = async (req, res) => {
  // User gửi mã refresh token kèm theo trong body
  const refreshTokenFromClient = req.body.refreshToken;
  // debug("tokenList: ", tokenList);
  
  // Nếu như tồn tại refreshToken truyền lên và nó cũng nằm trong tokenList của chúng ta
  if (refreshTokenFromClient && (tokenList[refreshTokenFromClient])) {
    try {
      // Verify kiểm tra tính hợp lệ của cái refreshToken và lấy dữ liệu giải mã decoded 
      const decoded = await jwtHelper.verifyToken(refreshTokenFromClient, refreshTokenSecret);

      // Thông tin user lúc này các bạn có thể lấy thông qua biến decoded.data
      // có thể mở comment dòng debug bên dưới để xem là rõ nhé.
      // debug("decoded: ", decoded);
      const userFakeData = decoded.data;

      debug(`Thực hiện tạo mã Token trong bước gọi refresh Token, [thời gian sống vẫn là 1 giờ.]`);
      const accessToken = await jwtHelper.generateToken(userFakeData, accessTokenSecret, accessTokenLife);

      // gửi token mới về cho người dùng
      return res.status(200).json({accessToken});
    } catch (error) {
      // Lưu ý trong dự án thực tế hãy bỏ dòng debug bên dưới, mình để đây để debug lỗi cho các bạn xem thôi
      debug(error);

      res.status(403).json({
        message: 'Invalid refresh token.',
      });
    }
  } else {
    // Không tìm thấy token trong request
    return res.status(403).send({
      message: 'No token provided.',
    });
  }
};

module.exports = {
  login: login,
  refreshToken: refreshToken,
}

File: src/controllers/FriendController.js

Trong file FriendController.js này mình viết một controller đơn giản friendLists – lấy ra danh sách bạn bè của người dùng.
Đây cũng sẽ là controller mà sau khi chúng ta xác thực người dùng thành công thì mới cho phép lấy thông tin bạn bè của người dùng rồi trả kết quả về.

/**
 * Created by trungquandev.com's author on 16/10/2019.
 * src/controllers/Friend.js
 */
const debug = console.log.bind(console);

let friendLists = (req, res) => {
  debug(`Xác thực token hợp lệ, thực hiện giả lập lấy danh sách bạn bè của user và trả về cho người dùng...`);
  // Lưu ý khi làm thực tế thì việc lấy danh sách này là query tới DB để lấy nhé. Ở đây mình chỉ mock thôi.
  const friends = [
    {
      name: "Cat: Russian Blue",
    },
    {
      name: "Cat: Maine Coon",
    },
    {
      name: "Cat: Balinese",
    },
  ];
  return res.status(200).json(friends);
}

module.exports = {
  friendLists: friendLists,
};

File: src/routes/api.js

File api.js này các bạn để ý cho mình dòng router.use(AuthMiddleWare.isAuth);
Tất cả các api các bạn khai báo dưới dòng này đều sẽ chạy qua cái isAuth trong AuthMiddleware để kiểm tra xác thực cái token hợp lệ thì mới cho đi xử lý tiếp sang bên controller nhé.

/**
 * Created by trungquandev.com's author on 16/10/2019.
 * src/routes/api.js
 */
const express = require("express");
const router = express.Router();
const AuthMiddleWare = require("../middleware/AuthMiddleware");
const AuthController = require("../controllers/AuthController");
const FriendController = require("../controllers/FriendController");

/**
 * Init all APIs on your application
 * @param {*} app from express
 */
let initAPIs = (app) => {
  router.post("/login", AuthController.login);
  router.post("/refresh-token", AuthController.refreshToken);

  // Sử dụng authMiddleware.isAuth trước những api cần xác thực
  router.use(AuthMiddleWare.isAuth);
  // List Protect APIs:
  router.get("/friends", FriendController.friendLists);
  // router.get("/example-protect-api", ExampleController.someAction);

  return app.use("/", router);
}

module.exports = initAPIs;

File: src/server.js

Cuối cùng, vẫn là phong cách đặt tên quen thuộc của mình, main file sẽ là file server.js để chạy ứng dụng node của chúng ta như sau:

/**
 * Created by trungquandev.com's author on 16/10/2019.
 * src/server.js
 */
const express = require("express");
const app = express();
const initAPIs = require("./routes/api");

// Cho phép các api của ứng dụng xử lý dữ liệu từ body của request
app.use(express.json());

// Khởi tạo các routes cho ứng dụng
initAPIs(app);

// chọn một port mà bạn muốn và sử dụng để chạy ứng dụng tại local
let port = 8017;
app.listen(port, () => {
  console.log(`Hello trungquandev.com, I'm running at localhost:${port}/`);
});

Phần code đã xong, bây giờ chúng ta sẽ đi test ứng dụng nhé, mình sẽ chạy ứng dụng lên và dùng Postman để call các api:

nodejs-authenticate-with-jsonwebtoken-jwt-trungquandev-00

Đầu tiên là api POST: /login

nodejs-authenticate-with-jsonwebtoken-jwt-trungquandev-01

Các bạn có thể copy cái accessToken lấy được ở trên, sau đó vào trang https://jwt.io paste để kiểm tra kết quả như thế này:

Tiếp theo chúng ta sử dụng accessToken lấy được ở trên để gọi api GET: /friends

nodejs-authenticate-with-jsonwebtoken-jwt-trungquandev-02

Ví dụ vài trường hợp chúng ta không truyền lên token hoặc truyền token linh tinh, truyền token hết hạn lên api GET: /friends

nodejs-authenticate-with-jsonwebtoken-jwt-trungquandev-03
nodejs-authenticate-with-jsonwebtoken-jwt-trungquandev-04

Khi accessToken hết hạn (1 tiếng như ví dụ mình đang làm) thì phía client sẽ lại gọi lên cái api POST: /refresh-token để làm mới lại accessToken

nodejs-authenticate-with-jsonwebtoken-jwt-trungquandev-05

Một vài giải thích quan trọng cuối cùng:

Như trong ví dụ mình có đặt 2 thời gian sống khác nhau cho accessToken (1 tiếng)refreshToken (10 năm), là bởi vì các bạn cứ để ý đơn giản cái app facebook rất ít khi bị trường hợp logout và đăng nhập lại đúng không? Cơ chế token mà mình làm trong bài này cũng như vậy, để đảm bảo tính liên tục cho ứng dụng nên mình để thời gian cái refreshToken là 10 năm.

Giả sử trường hợp 10 năm sau thằng người dùng mới dùng lại app đi thì bắt nó đăng nhập, còn nếu nó thường xuyên sử dụng app thì không phải đăng nhập. Và trong khoảng thời gian sử dụng app thì đôi khi người dùng cũng sẽ tự logout tài khoản và đăng nhập lại vì một lý do nào đó, nên lúc này refreshToken lại được làm mới, chính vì vậy mà có thể nói cơ chế này là đăng nhập mãi mãi =))))))

Một cái nữa, khi mà các bạn đã lưu trữ accessToken, refreshToken của người dùng trên server rồi, thì đôi khi có những nghiệp vụ mà chúng ta cần đứng từ phía người quản trị ứng dụng, bắt buộc phải force logout một tài khoản người dùng nào đó, lúc này đơn giản chỉ cần tìm và xóa 2 cái token của thằng người dùng đó trên hệ thống là được.


3. Full source code trên Github

Vậy là bài hôm nay chúng ta đã cùng nhau hoàn thiện về ý tưởng và cách triển khai cho việc xác thực người dùng sử dụng JWT Token, RefreshToken rồi.

Mình có để full source code của bài hôm nay ở repo này cho các bạn tham khảo nhé, nếu thấy bài viết bổ ích, hãy ủng hộ bằng cách cho mình 1 star trên repo này để mình có động lực tiếp tục viết những bài viết chất lượng nha, cảm ơn các bạn.
https://github.com/trungquan17/nodejs-jwt-authenticate-user


Cảm ơn các bạn đã dành thời gian đọc bài viết.

Xin chào và hẹn gặp lại các bạn ở những bài viết tiếp theo.

Best Regards – Trung Quân – Green Cat


Tham khảo kiến thức:

https://www.npmjs.com/package/jsonwebtoken
https://softwareontheroad.com/nodejs-jwt-authentication-oauth/
https://codetheworld.io/nodejs-xac-thuc-nguoi-dung-su-dung-jwt-va-co-che-refresh-token.html

“Thanks for awesome knowledges.”

trungquandev-img-modal

Khóa học lập trình làm việc thực tế:

Nếu các bạn thấy bài viết của mình có ích thì hãy ủng hộ mình bằng cách tham khảo bài viết giới thiệu khóa học cực kỳ chất lượng và chính chủ dưới đây của mình nhé, cảm ơn các bạn ^^

nodejs-mongodb-messenger-realtime-course-trungquandev
Node.js và MongoDB - Xây dựng một ứng dụng Messenger trò chuyện trực tuyến.