Hướng dẫn tạo Secure REST API trong Node.js

3
Dịch vụ dạy kèm gia sư lập trình

Ở 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.

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
  • email
  • 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" : "support@vntalking.com",
   "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

Nguồn tham khảo: Toptal – Creating a Secure REST API in Node.js, Node.JS Official document

Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng
Bài trướcTổ chức mã nguồn tối ưu cho dự án React Native lớn
Bài tiếp theoCái giá để trở thành lập trình viên giỏi là gì?
Tên đầy đủ là Dương Anh Sơn. Tốt nghiệp ĐH Bách Khoa Hà Nội. Mình bắt đầu nghiệp coder khi mà ra trường chẳng xin được việc đúng chuyên ngành. Mình tin rằng chỉ có chia sẻ kiến thức mới là cách học tập nhanh nhất. Các bạn góp ý bài viết của mình bằng cách comment bên dưới nhé !

3
Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Mới nhất Cũ nhất Nhiều voted nhất
Thông báo
Mongker
Guest
Mongker

Pro hướng dẫn thêm gửi token về bằng cookies nữa đi.

Việt Bình
Guest
Việt Bình

Mình cũng đang loay hoay về vụ secure api này.