Nodejs: Xác thực người dùng sử dụng JWT và cơ chế Refresh Token

https://codetheworld.io/nodejs-xac-thuc-nguoi-dung-su-dung-jwt-va-co-che-refresh-token.html

Ứng dụng Nodejs xác thực sử dụng JWT(Json Web Token) rất hữu ích khi bạn đang xây dựng một ứng dụng cho phép người dùng xác thực từ nhiều thiết bị (web app, mobile app…).
Một ứng dụng sử dụng xác thực bằng token hoạt động như thế nào:
  • Người dùng đăng nhập vào hệ thống và sau khi xác thực thành công, người dùng nhận được một mã token duy nhất và mã này có thời hạn sử dụng(ví dụ 15 phút).
  • Trong mỗi lần gọi API tiếp theo, người dùng phải đính kèm mã token trong request để truy cập các tài nguyên của hệ thống.
  • Khi hết thời gian, người dùng phải đăng nhập lại để nhận mã token mới.
Bước cuối cùng gây ra nhiều khó chịu, chúng ta không thể bắt người dùng đăng nhập lại mỗi khi mã token bị hết hạn.
Chúng ta có 2 cách để giải quyết vấn đề trên:
  1. Tăng thời gian hết hạn của token
  2. Sử dụng Refresh token để yêu cầu một token mới
Trong bài viết này, mình hướng dẫn xây dựng một ứng dụng Nodejs có bước xác thực người dùng và sử dụng giải pháp Refresh token để xử lý trường hợp mã token bị hết hạn.
Chú ý: Code trong bài viết chỉ nên sử dụng để giải thích, không nên sử dụng trong các ứng dụng thực tế.

Khởi tạo project

Chúng ta sẽ đi thẳng vào việc code xây dựng ứng dụng. Mình sẽ giải thích trên các đoạn code.
Chúng ta cần tạo mới một thư mục, thư mục này sẽ chứa toàn bộ code của dự án.
Từ phần này về sau, các câu lệnh đều được chạy trong thư mục này
Sử dụng câu lệnh dưới đây để tạo mới một ứng dụng Nodejs:
  • npm init --y
Cài đặt các thư viện cần thiết
Chúng ta sẽ cần một vài thư viện để ứng dụng có thể chạy được. Chạy câu lệnh bên dưới để cài đặt chúng:
  • npm i --S express body-parser jsonwebtoken
Các thư viện đã được cài đặt, nội dung file package.json cũng đã được cập nhật, chúng ta sẽ tới phần tiếp theo.

Gitignore

Bạn cần thêm tệp này để tránh các thư mục hay file nhất định được thêm vào Git Repository.
Bạn cần tạo mới file .gitignore và thêm dòng dưới đây:
  • node_modules/
Điều này có nghĩa chúng ta sẽ không thêm thư mục node_modules vào git repo.
Ok, tới lúc viết code rồi!

Khởi tạo http server và các route cơ bản

Chúng ta sẽ sử dụng express để tạo mới một http server bằng Nodejs. Đây là nội dung file app.js
  • const express = require('express');
  • const bodyParser = require('body-parser');
  • const jwt = require('jsonwebtoken');
  • const router = express.Router();
  • const config = require('./config');
  • const utils = require('./utils');
  • const tokenList = {};
  • const app = express();
  • router.get('/', (req, res) => {
  • res.send('Ok');
  • });
  • /**
  • * Đăng nhập
  • * POST /login
  • */
  • router.post('/login', (req, res) => {
  • const postData = req.body;
  • const user = {
  • "email": postData.email,
  • "name": postData.name
  • }
  • // Thực hiện việc kết nối cơ sở dữ liệu (hay tương tự) để kiểm tra thông tin username and password
  • // Đăng nhập thành công, tạo mã token cho user
  • const token = jwt.sign(user, config.secret, {
  • expiresIn: config.tokenLife,
  • });
  • // Tạo một mã token khác - Refresh token
  • const refreshToken = jwt.sign(user, config.refreshTokenSecret, {
  • expiresIn: config.refreshTokenLife
  • });
  • // Lưu lại mã Refresh token, kèm thông tin của user để sau này sử dụng lại
  • tokenList[refreshToken] = user;
  • // Trả lại cho user thông tin mã token kèm theo mã Refresh token
  • const response = {
  • token,
  • refreshToken,
  • }
  • res.json(response);
  • })
  • /**
  • * Lấy mã token mới sử dụng Refresh token
  • * POST /refresh_token
  • */
  • router.post('/refresh_token', async (req, res) => {
  • // User gửi mã Refresh token kèm theo trong body
  • const { refreshToken } = req.body;
  • // Kiểm tra Refresh token có được gửi kèm và mã này có tồn tại trên hệ thống hay không
  • if ((refreshToken) && (refreshToken in tokenList)) {
  • try {
  • // Kiểm tra mã Refresh token
  • await utils.verifyJwtToken(refreshToken, config.refreshTokenSecret);
  • // Lấy lại thông tin user
  • const user = tokenList[refreshToken];
  • // Tạo mới mã token và trả lại cho user
  • const token = jwt.sign(user, config.secret, {
  • expiresIn: config.tokenLife,
  • });
  • const response = {
  • token,
  • }
  • res.status(200).json(response);
  • } catch (err) {
  • console.error(err);
  • res.status(403).json({
  • message: 'Invalid refresh token',
  • });
  • }
  • } else {
  • res.status(400).json({
  • message: 'Invalid request',
  • });
  • }
  • });
  • /**
  • * Middleware xác thực người dùng dựa vào mã token
  • * @param {*} req
  • * @param {*} res
  • * @param {*} next
  • */
  • const TokenCheckMiddleware = async (req, res, next) => {
  • // Lấy thông tin mã token được đính kèm trong request
  • const token = req.body.token || req.query.token || req.headers['x-access-token'];
  • // decode token
  • if (token) {
  • // Xác thực mã token và kiểm tra thời gian hết hạn của mã
  • try {
  • const decoded = await utils.verifyJwtToken(token, config.secret);
  • // Lưu thông tin giã mã được vào đối tượng req, dùng cho các xử lý ở sau
  • req.decoded = decoded;
  • next();
  • } catch (err) {
  • // Giải mã gặp lỗi: Không đúng, hết hạn...
  • console.error(err);
  • return res.status(401).json({
  • message: 'Unauthorized access.',
  • });
  • }
  • } else {
  • // Không tìm thấy token trong request
  • return res.status(403).send({
  • message: 'No token provided.',
  • });
  • }
  • }
  • router.use(TokenCheckMiddleware);
  • router.get('/profile', (req, res) => {
  • // all secured routes goes here
  • res.json(req.decoded)
  • })
  • app.use(bodyParser.json());
  • app.use('/api', router);
  • app.listen(config.port || process.env.PORT || 3000);
Nội dung file utils.js:
  • const jwt = require('jsonwebtoken');
  • module.exports = {
  • verifyJwtToken: (token, secretKey) => {
  • return new Promise((resolve, reject) => {
  • jwt.verify(token, secretKey, (err, decoded) => {
  • if (err) {
  • return reject(err);
  • }
  • resolve(decoded);
  • });
  • });
  • }
  • }
File này cung cấp một hàm để xác thực các mã token.
Mình chuyển từ một callback function thành Promise để dễ sử dụng.
Đây là nội dung file config.js
  • module.exports = {
  • "secret": "s0me-secr3t-goes-here",
  • "refreshTokenSecret": "some-s3cret-refre2h-token",
  • "port": 3000,
  • "tokenLife": 900, // 15 phút
  • "refreshTokenLife": 86400 // một ngày
  • }
Mình sử dụng 2 chuỗi bí mật và 2 thời gian hết hạn khác nhau cho 2 loại token. Sau khi đã có 2 loại token, mình tiền hành lưu lại thông tin Refresh token vào một biến: Sử dụng chính token đó làm key và value là thông tin người dùng
  • tokenList[refreshToken] = user;
Chú ý: Bạn nên sử dụng một nơi lưu trữ ổn định thay vì một biến cục bộ trên product, bạn có thể dùng Redis.
Với route POST refresh_tokenchúng ta sẽ lấy thông tin Refresh token từ trong body client gửi lên, nếu token này tồn tại, chúng ta kiểm tra tính hợp lệ của nó (Có tồn tại trên hệ thống hay không,  có phải do hệ thống sinh ra hay không).
Nếu nhận được một mã Refresh token hợp lệ, chúng ta sẽ tạo mới một mã token và gửi mã mới này về cho người dùng. Bằng cách này người dùng không cần phải đăng nhập lại.

Middleware xác thực cho các API cần bảo vệ

Chúng ta cần có một phần code luôn luôn phải được thực thi để kiểm tra mã token mà client gửi kèm theo các request có hợp lệ hay không.
Như bạn thấy, mình có tạo một hàm để làm việc này:
  • /**
  • * Middleware xác thực người dùng dựa vào mã token
  • * @param {*} req
  • * @param {*} res
  • * @param {*} next
  • */
  • const TokenCheckMiddleware = async (req, res, next) => {
  • // Lấy thông tin mã token được đính kèm trong request
  • const token = req.body.token || req.query.token || req.headers['x-access-token'];
  • // decode token
  • if (token) {
  • // Xác thực mã token và kiểm tra thời gian hết hạn của mã
  • try {
  • const decoded = await utils.verifyJwtToken(token, config.secret);
  • // Lưu thông tin giã mã được vào đối tượng req, dùng cho các xử lý ở sau
  • req.decoded = decoded;
  • next();
  • } catch (err) {
  • // Giải mã gặp lỗi: Không đúng, hết hạn...
  • console.error(err);
  • return res.status(401).json({
  • message: 'Unauthorized access.',
  • });
  • }
  • } else {
  • // Không tìm thấy token trong request
  • return res.status(403).send({
  • message: 'No token provided.',
  • });
  • }
  • }
Thực hiện kiểm tra mã token một cách đơn giản. Bằng middleware này, mọi route được đăng ký sau đó đều được bảo vệ, mỗi request đều cần một mã token hợp lệ để có thể truy cập được các tài nguyên của hệ thống.

Test ứng dụng

Tới lúc chúng ta sẽ xem ứng dụng hoạt động như thế nào.
Khởi động ứng dụng bằng câu lệnh:
  • node app.js
Mở công cụ làm việc với API mà bạn yêu thích lên và test, mình hay sử dụng Postman.
Mẹo: Mình sẽ sử dụng curl để mô tả các request ở phần dưới, các bạn có thể copy đoạn request bằng curl và import vào Postman.
Đầu tiên sẽ là API login POST http://localhost:3000/api/login
  • curl -X POST \
  • http://localhost:3000/api/login \
  • -H 'Content-Type: application/json' \
  • -d '{
  • "email": "hoang.dv@outlook.com",
  • "name": "hoangdv"
  • }'
Kết quả nhận được sẽ tương tự:
  • {
  • "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhvYW5nLmR2QG91dGxvb2suY29tIiwibmFtZSI6ImhvYW5nZHYiLCJpYXQiOjE1NjAzNTIyOTAsImV4cCI6MTU2MDM1MzE5MH0.xyna1mYwRbRo-Jdo_ZiwjnOJHpSscHrg93ZbIDawNfo",
  • "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhvYW5nLmR2QG91dGxvb2suY29tIiwibmFtZSI6ImhvYW5nZHYiLCJpYXQiOjE1NjAzNTIyOTAsImV4cCI6MTU2MDQzODY5MH0.ZrsVpCfzUOsch_s-51uIdavzMH-N-jmBD3dQz3ssFkw"
  • }
Bây giờ, hãy copy nội dung của token, nó sẽ được dùng để xác thực khi gọi các api cần xác thực.
Như đoạn code của middleware const token = req.body.token || req.query.token || req.headers['x-access-token']; điều này có nghĩa chúng ta có thể gửi kèm token ở trong body, query string hoặc header của request, mình sẽ lấy ví dụ token được gửi kèm trong header của request: (Mình dùng giá trị token tại thời điểm viết bài)
  • curl -X GET \
  • http://localhost:3000/api/profile \
  • -H 'x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhvYW5nLmR2QG91dGxvb2suY29tIiwibmFtZSI6ImhvYW5nZHYiLCJpYXQiOjE1NjAzNTIyOTAsImV4cCI6MTU2MDM1MzE5MH0.xyna1mYwRbRo-Jdo_ZiwjnOJHpSscHrg93ZbIDawNfo'
Kết quả, thông tin các nhân của mình(kèm theo một vài thông tin của mã jwt):
  • {
  • "email": "hoang.dv@outlook.com",
  • "name": "hoangdv",
  • "iat": 1560352290,
  • "exp": 1560353190
  • }
Có được thông tin này vì ở bước middleware, sau khi xác thực và giải mã token, mình đã gán thông tin giải mã được vào đối tượng req, ở trong route profile chỉ cần lấy thông tin đó ra trả về.
Nếu mình request POST /api/profile mà không có thông tin token thì sẽ nhận được http status là 403, kèm theo tin nhắn:
  • {
  • "message": "No token provided."
  • }
nếu mã token không đúng hoặc bị hết hạn, http status là 401, kèm theo tin nhắn:
  • {
  • "message": "Unauthorized access."
  • }
Rồi, khi token bị hết hạn (các bạn có thể để thời gian sống của token ngắn một chút để có thể test dễ dàng), chúng ta sẽ gọi API sử dụng Refresh token để lấy một mã token mới:
  • curl -X POST \
  • http://localhost:3000/api/refresh_token \
  • -H 'Content-Type: application/json' \
  • -d '{
  • "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhvYW5nLmR2QG91dGxvb2suY29tIiwibmFtZSI6ImhvYW5nZHYiLCJpYXQiOjE1NjAzNTMyNjMsImV4cCI6MTU2MDQzOTY2M30.-TUGYfNdrYq8wphEsOzZXQvolgyOS88bvqGEAtieejM"
  • }'
kết quả, chúng ta nhận được một mã token mới hợp lệ:
  • {
  • "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhvYW5nLmR2QG91dGxvb2suY29tIiwibmFtZSI6ImhvYW5nZHYiLCJpYXQiOjE1NjAzNTMyNjMsImV4cCI6MTU2MDQzOTY2M30.-TUGYfNdrYq8wphEsOzZXQvolgyOS88bvqGEAtieejM"
  • }

Kết luận

Xây dựng một ứng dụng web bằng Nodejs thì việc phải đảm bảo các cơ chế xác thực người dùng là không thể thiếu. Bạn nên xây dựng kèm cơ chế làm mới mã xác thực token, việc này giúp cho ứng dụng phía client có thể hoạt động một cách liền mạch.
Mình đã trình bày những điều cơ bản nhất, hy vọng bạn có thể vận dụng được gì đó trong dự án của mình.
Toàn bộ code trong bài viết: Gist

Nhận xét

  1. HAFELE HH-TG60E 539.81.073 là dòng máy hút mùi âm tủ, thiết kế sang trọng hiện đại với sự kết hợp giữa inox và mặt kính đen cường lực, không gian bếp của bạn trở nên nổi bật hơn, hiện đại hơn. Hoạt động êm ái với các tính năng vượt trội, máy hút mùi HAFELE HH-TG60E 539.81.073 là sản phẩm đáng để bạn quan tâm. Máy hút mùi HAFELE HH-TG60E 539.81.073 thiết kế âm tủ gọn gàng kết hợp giữa inox và mặt kính đen tạo nên vẻ sang trọng và hiện đại cho không gian bếp. Máy có công suất hút mạnh mẽ lên đến 800m3/h loại bỏ hoàn toàn mùi thức ăn khi đun nấu. Mặt khác, máy được thiết kế kết hợp 2 tính năng hút xả thông gió và tuần hoàn khép kín nên nó là dòng máy phù hợp cho rất nhiều không gian bếp nhất là đối với không gian bếp nhỏ hẹp và kín.

    HAFELE HH-TG90E 539.81.075 là dòng máy hút mùi âm tủ, thiết kế sang trọng hiện đại với sự kết hợp giữa inox và mặt kính đen cường lực, không gian bếp của bạn trở nên nổi bật hơn, hiện đại hơn. Hoạt động êm ái với các tính năng vượt trội, máy hút mùi HAFELE HH-TG90E 539.81.075 là sản phẩm đáng để bạn quan tâm. Máy hút mùi HAFELE HH-TG90E 539.81.075 thiết kế âm tủ gọn gàng kết hợp giữa inox và mặt kính đen tạo nên vẻ sang trọng và hiện đại cho không gian bếp. Máy có công suất hút mạnh mẽ lên đến 800m3/h loại bỏ hoàn toàn mùi thức ăn khi đun nấu. Mặt khác, máy được thiết kế kết hợp 2 tính năng hút xả thông gió và tuần hoàn khép kín nên nó là dòng máy phù hợp cho rất nhiều không gian bếp nhất là đối với không gian bếp nhỏ hẹp và kín. Sự tinh tế, sang trọng trong thiết kế với những đường nét nổi bật giúp cho máy hút mùi HAFELE HH-TG90E 539.81.075 có tính thẩm mỹ đặc trưng riêng. Máy có kích thước nhỏ gọn lắp đặt kiểu âm tủ làm cho không gian bếp của bạn gọn gàng hơn nhất là đối với những không gian bếp nhỏ hẹp. Sự kết hợp giữa inox và mặt kính đen giúp dễ dàng vệ sinh làm sạch. Đặc biệt, máy hút mùi HAFELE HH TG90E 539.81.075 kết hợp 2 tính năng hút xả và tuần hoàn khép kín với 3 mức công suất hút mạnh mẽ lên đến 800m3/h bạn có thể lựa chọn tùy vào không gian bếp của mình.

    Trả lờiXóa

Đăng nhận xét

Bài đăng phổ biến từ blog này

Các lệnh cơ bản MongoDB

Truy vấn dữ liệu trong MongoDB

Cách tạo một project Express với express-generator trong Window