Ở các bài viết trước, VNTALKING đã hướng dẫn các bạn cách xây dựng REST API bằng Node.js + Express. Các bạn có thể xem trước khi quay lại bài viết này:
🙋 Tạo RESTful API đơn giản bằng Nodejs + MongoDB
Tuy nhiên, còn một công đoạn quan trọng mà bạn cần phải làm trước khi đưa sản phẩm thành product. Đó chính là bảo mật các REST API, hay nói cách khác là tạo secure REST API.
Mục tiêu của việc tạo secure REST API là để bạn có thể cấp phép cho một ứng dụng cụ thể nào đó, được phép sử dụng API.
Bài viết này, mình sẽ hướng dẫn các bạn tạo secure REST API bằng Node.js. Cụ thể, chúng ta sẽ cùng nhau thực hành tạo API để ứng dụng truy cập vào tài nguyên quản lý người dùng trong hệ thống.
Nội dung chính của bài viết
Secure REST API là gì?
API nói chung, REST API nói riêng, là phương thức để cung cấp một dịch vụ mà phía server có thể thực hiện.
Vấn đề phát sinh ra ở đây là: Bên dịch vụ cung cấp API muốn xác định được thông tin của phía sử dụng API. Mục đích có thể là:
- Thu phí sử dụng dịch vụ chẳng hạn,
- Hoặc giới hạn chỉ cho một số ứng dụng cụ thể được phép sử dụng.v.v…
Đó là lúc chúng ta cần tạo secure Rest API. Thực ra secure REST API vẫn là REST API đó thôi, chỉ là chúng ta cần thêm một luồng để xác thực danh tính người sử dụng API đó, ví dụ thông qua quá trình login chẳng hạn.
Giới thiệu dự án tạo secure REST API trong bài viết
Một người dùng (user) có những thông tin sau:
- id (tự động generated bởi UUID)
- firstName
- lastName
- password
- permissionLevel (Sử dụng để phân quyền User)
Danh sách các APIs sẽ xây dựng:
- [POST] endpoint/users
- [GET] endpoint/users (danh sách users)
- [GET] endpoint/users/:userId (lấy một user cụ thể)
- [PATCH] endpoint/users/:userId (cập nhật thông tin một user cụ thể )
- [DELETE] endpoint/users/:userId (xóa một user cụ thể)
Ngoài ra, chúng ta sử dụng JWT (JSON Web Token) cho access token. Để làm được điều này, chúng ta sẽ tạo thêm một resource nữa, gọi là auth, sử dụng email và password để tạo chuỗi token dùng để xác thực giữa client và server.
Chúng ta bắt đầu thực hành nhé!
Cài đặt môi trường
Để thực hiện dự án này, chúng ta cần phải cài đặt môi trường phát triển gồm có:
- Node.js
- Express
- MongoDB
Mình không hướng dẫn cụ thể cách cài đặt trong bài viết này, bạn có thể tham khảo tại đây: Cài đặt NodeJs trên Window, Ubuntu chi tiết
Sau khi cài đặt xong, môi trường phát triển đã chuẩn bị xong. Bước tiếp là tạo mới một dự án Node.JS: npm init
Sau khi tạo xong dự án, để thống nhất danh sách các dependencies sử dụng trong dự án, bạn có thể copy nội dùng package.json và paste vào dự án của bạn
{ "name": "rest-api-tutorial", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "VNTALKING", "license": "ISC", "dependencies": { "body-parser": "1.7.0", "express": "^4.8.7", "jsonwebtoken": "^7.3.0", "moment": "^2.17.1", "moment-timezone": "^0.5.13", "mongoose": "^5.1.1", "node-uuid": "^1.4.8", "swagger-ui-express": "^2.0.13", "sync-request": "^4.0.2" } }
Cuối cùng, chúng ta tạo thêm 3 thư mục, tương ứng 3 modules:
- “common” (xử lý tất cả dịch vụ và thông tin được chia sẻ giữa các module)
- “users” (tất cả mọi thứ liên quan tới users)
- “auth” (xử lý việc tạo JWT và luồng login – xác thực)
Tạo User Module
Chúng ta sử dụng thư viện Mongoose trong dự án để tương tác với MongoDB.
Đầu tiên, tạo User schema: /users/models/users.model.js
const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });
Sau khi định nghĩa xong schema, bạn dễ dàng attach schema đó vào user model:
const userModel = mongoose.model('Users', userSchema);
Sau khi định nghĩa model xong, bạn sẽ sử dụng model cho tất cả các thao tác CRUD với DB.
API thêm User
Ở phần này, chúng ta sẽ tạo API: [POST] endpoint/users
Chúng ta định nghĩa route cho api này tại users/routes.config.js
app.post('/users', [ UsersController.insert ]);
Phần comtroller, chúng ta sẽ xử lý mã hóa giá trị password, mở /users/controllers/users.controller.js
exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest("base64"); req.body.password = salt + "$" + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); };
Ở đoạn code trên, chúng có gọi hàm UserModel.createUser(req.body)
để lưu dữ liệu vào DB. Nhưng mà chúng ta chưa có viết hàm này, bạn mà chạy là bị lỗi compile luôn.Mở users/models/users.model.js
và thêm đoạn này:
exports.createUser = (userData) => { const user = new User(userData); return user.save(); };
API lấy thông tin một User
Ở phần này, chúng ta sẽ tạo API: [GET] endpoint/users/:userId
Bạn mở /users/routes/config.js
để định nghĩa router:
app.get('/users/:userId', [ UsersController.getById ]);
Sau đó, trong controller /users/controllers/users.controller.js
exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };
Cuối cùng thì sử dụng hàm findById
trong model (/users/models/users.model.js
)để tìm User trong DB.
exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); };
Vậy là xong cho API rồi đấy.
Các API cho update, xóa và lấy danh sách users, các bạn làm tương tự. Có thể tham khảo trong mã nguồn đầy đủ, mình để ở dưới bài viết.
Chúng ta chuyển sang phần trọng tâm của bài viết, đó là xây dựng lớp bảo mật cho API.
Tạo Auth module
Trước khi chúng ta có thể bảo mật user module bằng cách thực hiện phân quyền và xác thực. Chúng ta cần phải tạo mã access token cho người/ứng dụng sử dụng API.
Như mình đã đề cập ở trên, chúng ta sẽ sử dụng JWT để tạo access token. Trong bài viết này, nó là một chuỗi JSON được tạo dựa trên thông email và password của người dùng. Tất nhiên, chúng ta sẽ bổ sung thêm thời hạn mà mã access token này hết hạn để gia tăng tính bảo mật.
Đầu tiên, chúng ta định nghĩa route (POST: /auth
), dữ liệu trong body gồm thông tin email và password, ví dụ như dưới đây:
{ "email" : "[email protected]", "password" : "s3cr3tp4sswo4rd2" }
Trước khi chuyển sang controller, chúng ta tạo một middleware để xác thực (/authorization/middlewares/verify.user.middleware.js
)
exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); };
Ok, giờ thì chuyển sang controller và tạo mã JWT
exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest("base64"); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = new Buffer(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } };
Mặc dù trong bài viết này, mình không có implement việc tạo mới access token khi mã token cũ hết hạn. Nhưng mình cũng để sẵn code để bạn thêm vào trong tương lai một cách dễ dàng.
Cuối cùng là tạo route và gọi middleware tương ứng
app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);
Kết quả thu được mã access toke giống như thế này
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY", "refreshToken": "U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ }
Sau khi ứng dụng client có mã token này, nó có thể gửi mã token đó lên server mỗi khi truy cập vào API, thay vì phải gửi email hay mật khẩu.
Tạo Permissions và Validations Middleware
Đầu tiên, chúng ta cần xác định “ai” được phép sử dụng user resource thông qua API này. Đây là một user case mà chúng ta cần xử lý:
- Người dùng chưa đăng ký: được phép sử dụng API tạo mới user
- Người dùng/quản trị viên đã đăng nhập: được phép sử dụng API hiển thị danh sách user, update user.
- Quản trị viên đã đăng nhập: được phép xóa user.
Sau khi xác định được các user case, bước tiếp theo là tạo middleware để luôn validate thông tin user nếu user này có một mã JWT hợp lệ.
Mở /common/middlewares/auth.validation.middleware.js
exports.validJWTNeeded = (req, res, next) => { if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } };
Trong này, có các mã HTTP code để xử lý lỗi:
- HTTP 401: cho invalid request
- HTTP 403: cho một request hợp lệ nhưng token không hợp lệ. Hoặc token hợp lệ nhưng permision không đủ thẩm quyền truy cập vào API này.
Ví dụ một middleware cho phân quyền:
exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; };
Công việc cuối cùng là thêm authentication middleware vào route
app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]);
Vậy là xong rồi đấy
Tạm kết
Trên đây là hướng dẫn một phương pháp để bạn bảo mật cho REST API của mình. Mình hi vọng, qua bài viết này, các bạn hiểu hơn về Node.js và cách REST API trên Node.js
Toàn bộ mã nguồn minh họa trong bài viết này, các bạn tải ở đây nhé:
Đừng quên để lại vài lời bình luận làm động lực cho tác giả nhé
🔥 Đọc thêm về Node.js
- Xây dựng ứng dụng web với NodeJS + ExpressJS
- 7 sai lầm khi học Nodejs hay mắc phải
- Unit Test cho Nodejs với Mocha – Step by Step
Nguồn tham khảo: Toptal – Creating a Secure REST API in Node.js, Node.JS Official document
Pro hướng dẫn thêm gửi token về bằng cookies nữa đi.
Vâng, mình sẽ sớm bổ sung vào bài viết ạ
Mình cũng đang loay hoay về vụ secure api này.