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:
- Phân tích bài toán đặt ra
- Lao vào code
- 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 accessToken và refreshToken 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à:
express và jsonwebtoken
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 token và verifyToken – 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ẽ bao 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:
Đầu tiên là api POST: /login
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
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
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
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/trungquandev/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.”