Nội dung bài viết
Video học lập trình mỗi ngày
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 indexRouter
và usersRouter
. 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
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
Khi login thành công thì client sẽ nhận được 2 token bao gồm token
và refreshToken
Sau đó bạn sử dụng token
để truy cập vào list user.
Sau thời gian mà chúng ta đã setting trong file config thì token sẽ hết hạn như thế này.
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ó
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.