JSON Web Token (JWT) - Thực hành sử dụng refresh token khi token hết hạn với nodejs và express js

Nội dung bài viết

JSON Web Token (JWT) là một cơ chế bảo vệ tài nguyên có thể nói đến bây giờ nó phổ biến rộng rãi đến mức nhà nhà, người người ai cũng biết đến nó. Nhưng hiện tại qua nhiều diễn đàn, vẫn còn đâu đó những câu hỏi như làm sao lấy lại token mới nếu như hết hạn sử dụng refresh token?


Nếu như bạn đang cùng câu hỏi đó thì rất may mắn cho tôi có cơ hội để giúp bạn hiểu thông qua một bài thực hành Thực hành sử dụng refresh token khi token hết hạn với nodejs và express js. Bài này tôi sẽ hướng dẫn kỹ nhất có thể, và việc của bạn chỉ đọc code từ từ và cảm nhận, sau đó là nên clone code về rồi thực hành lại một lần nữa là ngon lành. 


Các bạn có thể kéo xuống dưới bài viết để clone CODE cho toàn bộ bài viết này. Nhưng hãy đọc những yêu cầu trước tiên.


Bài thực hành này không khó nhưng bạn phải cần hiểu những khái niệm sau như: 



Rất nhiều bạn nếu đã biết và đọc nhiều về tips and tricks javascript thì sẽ biết rằng trong blog javascript này có rất nhiều bài viết về JSON Web Token. Trong đó có tất cả những câu trả lời mà bạn muốn tìm hiểu nếu bạn là người mới. OK tôi chỉ nói một vài lời nhiêu đó thôi, còn để thời gian đi vào vấn đề chính của chúng ta.


Creating the Project


Như lời nói đầu ở đây tôi sử dụng nodejs + expressjs để thực hiện project này. Và tôi dùng Express application generator cho nhanh. Nếu bạn chưa biết về Express generator thì có thể install tại đây


Sau khi install thành công thì thực hiện command sau để tạo project Express generator


AnonyStick$ express --view=ejs refreshToken-demo
   create : refreshToken-demo/
   create : refreshToken-demo/public/
   create : refreshToken-demo/public/javascripts/
   create : refreshToken-demo/public/images/
   create : refreshToken-demo/public/stylesheets/
   create : refreshToken-demo/public/stylesheets/style.css
   create : refreshToken-demo/routes/
   create : refreshToken-demo/routes/index.js
   create : refreshToken-demo/routes/users.js
   create : refreshToken-demo/views/
   create : refreshToken-demo/views/error.ejs
   create : refreshToken-demo/views/index.ejs
   create : refreshToken-demo/app.js
   create : refreshToken-demo/package.json
   create : refreshToken-demo/bin/
   create : refreshToken-demo/bin/www

   change directory:
     $ cd refreshToken-demo

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=refreshtoken-demo:* npm start

Ở đây tôi tạo name project là refreshToken-demo và sử dụng template là ejs không giải thích kỹ nha, vì ở đây qua dễ rồi.


Creating Server and adding routes


var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var app = express();
//add them
var bodyParser = require('body-parser')

app.use(bodyParser.json()) 
app.use(bodyParser.urlencoded({ extended: true })) 

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');



// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Và đây là file app.js sau khi projetc được tạo ra ở đây không quan trọng vì chỉ khai báo package và file config mà thôi. Ở đây bạn chỉ chú ý đến indexRouterusersRouter. Tôi sẽ giải thích một chút ở đây. 


  • usersRouter đây là nơi chứa dữ liệu của Users. Do đó chỉ những người login thành công và có quyền mới có thể lấy được data này thông qua api dựa vào token. 
  • indexRouter đây là nơi mà router sẽ khai báo API login và API lấy lại token nếu như token hết hạn phải sử dụng refreshToken.


Create files


### /router/index.js

var express = require('express');
var router = express.Router();
const _CONF = require('../config')
var jwt = require('jsonwebtoken') 

var refreshTokens = {} ;// tao mot object chua nhung refreshTokens


/* GET home page. */
router.get('/', function(req, res, next) {
  return res.json({status: 'success', elements: 'Hello anonystick'})
});

/* LOGIN . */
router.post('/login', function(req, res, next) {
  const {username, password} = req.body;
  if(username === 'anonystick.com' && password === 'anonystick.com'){
    let user = {
      username: username,
      role: 'admin'
    }
    const token = jwt.sign(user, _CONF.SECRET, { expiresIn: _CONF.tokenLife }) ;//20 giay
    const refreshToken = jwt.sign(user, _CONF.SECRET_REFRESH, { expiresIn: _CONF.refreshTokenLife})

    const response = {
      "status": "Logged in",
      "token": token,
      "refreshToken": refreshToken,
    }

    refreshTokens[refreshToken] = response

    return res.json(response)
  }
  return res.json({status: 'success', elements: 'Login failed!!!'})

})

/* Get new token when jwt expired . */

router.post('/token', (req,res) => {
  // refresh the damn token
  const {refreshToken} = req.body
  // if refresh token exists
  if(refreshToken && (refreshToken in refreshTokens)) {
      const user = {
          username: 'anonystick.com',
          role: 'admin'
      }
      const token = jwt.sign(user, _CONF.SECRET, { expiresIn: _CONF.tokenLife})
      const response = {
          "token": token,
      }
      // update the token in the list
      refreshTokens[refreshToken].token = token
      res.status(200).json(response);        
  } else {
      res.status(404).send('Invalid request')
  }
})

module.exports = router;

### /routes/users.js

var express = require('express');
var router = express.Router();


router.use(require('../middleware/checkToken'))
/* GET users listing. */
router.get('/', function (req, res) {
  const users = [{
    username: 'Cr7',
    team: 'Juve',
  }, {
    username: 'Messi',
    team: 'Barca',
  }]
  res.json({ status: 'success', elements: users })
})

module.exports = router;

### config/index.js

const config = Object.freeze({
    SECRET:"SECRET_ANONYSTICK",
    SECRET_REFRESH: "SECRET_REFRESH_ANONYSTICK",
    tokenLife: 10,
    refreshTokenLife: 120
})

module.exports = config;

### middleware/checkToken.js

const jwt = require('jsonwebtoken')
const _CONF = require('../config/')

module.exports = (req, res, next) => {
  const token = req.body.token || req.query.token || req.headers['x-access-token']
  // decode token
  if (token) {
    // verifies secret and checks exp
    jwt.verify(token, _CONF.SECRET, function(err, decoded) {
        if (err) {
            console.error(err.toString());
            //if (err) throw new Error(err)
            return res.status(401).json({"error": true, "message": 'Unauthorized access.', err });
        }
        console.log(`decoded>>${decoded}`);
        req.decoded = decoded;
        next();
    });
  } else {
    // if there is no token
    // return an error
    return res.status(403).send({
        "error": true,
        "message": 'No token provided.'
    });
  }
}

Nhìn vào đoạn code thì không khó để hiểu nhưng ở đây tôi muốn bạn chú ý đến những đoạn code sau:

const token = jwt.sign(user, _CONF.SECRET, { expiresIn: _CONF.tokenLife }) ;//20 giay
const refreshToken = jwt.sign(user, _CONF.SECRET_REFRESH, { expiresIn: _CONF.refreshTokenLife})

Khi một tài khoản login thành công thì hệ thống sẽ sinh ra hai token đó là token và refreshToken. Đương nhiên thời gian sống của hai token này khác nhau vì sao thì tôi có nói trong bài viết trước kia. Và người đó sẽ nhận được và lưu trữ ở client. Về các lưu trữ token thì nên lưu ở đâu thì chúng tôi cũng có nói ở bài viết này. "Lưu trữ và bảo mật token jwt"

const response = {
    "status": "Logged in",
    "token": token,
    "refreshToken": refreshToken,
}

refreshTokens[refreshToken] = response

Và chúng tôi sẽ lưu trữ những refreshToken trên server dùng vào nhiều mục đích khác nhau như lấy lại token nếu hết hạn, hoặc chặn ngay những hành động hacker thì chiếm đoạt token của User. 


Tips: Tốt hơn hết bạn nên lưu trữ refreshToken ở redis vì khi reload server thì nó sẽ mất

router.use(require('../middleware/checkToken'))

Tiếp theo là tôi tạo một file middleware có tác dụng check token hợp lệ trước khi truy cập tài nguyên, nhìn vào bạn sẽ rõ hơn.

router.use(require('../middleware/checkToken'))
/* GET users listing. */
router.get('/', function (req, res) {
  const users = [{
    username: 'Cr7',
    team: 'Juve',
  }, {
    username: 'Messi',
    team: 'Barca',
  }]
  res.json({ status: 'success', elements: users })
})

Và nếu token không hợp lệ đương nhiên bạn không thể truy cập vào tài nguyên Users được đâu. Run code Sau khi clone code về bạn có thể dùng command để run như sau:

AnonyStick$ npm start

Và hãy mớ Postman lên để thử xem nhé. Đầu tiên tôi chưa login và tôi sẽ thử truy cập vào danh sách User

refresh Token là gì?

Bạn có thể nhìn thấy chúng ta không thể truy cập được. Bạn cũng có thể thử với một token tùm lum nào đó. Tiếp theo tôi sẽ login với tài khoản là anonystick.com

jwt là gì?

Khi login thành công thì client sẽ nhận được 2 token bao gồm tokenrefreshToken Sau đó bạn sử dụng token để truy cập vào list user.

token là gì?

Sau thời gian mà chúng ta đã setting trong file config thì token sẽ hết hạn như thế này.

token expired jwt

Khi hết hạn token thì chúng ta sẽ sử dụng post /token để lấy lại token mới sử dụng refreshToken mà login đã có

sử dụng refreshToken lấy token mới


Tóm lại


Về cơ bản thì cơ chế lấy lại token khi hết hạn khi dùng jwt là như vậy. Nhưng ở đây chi là quy trình khác với thực thế ở những chỗ sau như, khi hết hạn thì client tự động gửi refreshToken về server để lấy chứ không phải mình đi copy như vậy. Client phải tự động nhận được lỗi 401 invalid token và tự động gọi function làm mới token rồi chạy tiếp như chưa có chuyện gì xảy ra. Có thể ở bài viết sau tôi sẽ làm những đieuf này cho các bạn. 


DOWNLOAD CODE HERE

Có thể bạn đã bị missing