Hướng dẫn secure nodejs api - api nodejs an toàn

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

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:

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
7

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)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)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)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:

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
8

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:  

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
9

Chúng ta định nghĩa route cho api này tại

const userModel = mongoose.model('Users', userSchema);
0

app.post('/users', [
   UsersController.insert
]);

Phần comtroller, chúng ta sẽ xử lý mã hóa giá trị password, mở

const userModel = mongoose.model('Users', userSchema);
1

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

const userModel = mongoose.model('Users', userSchema);
2 để 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ở
const userModel = mongoose.model('Users', userSchema);
3 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:

const userModel = mongoose.model('Users', userSchema);
4

Bạn mở

const userModel = mongoose.model('Users', userSchema);
5 để định nghĩa router:

app.get('/users/:userId', [
    UsersController.getById
]);

Sau đó, trong controller

const userModel = mongoose.model('Users', userSchema);
1

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

const userModel = mongoose.model('Users', userSchema);
7 trong model (
const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
8)để 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 (

const userModel = mongoose.model('Users', userSchema);
9), 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 (

app.post('/users', [
   UsersController.insert
]);
0)

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
0

Ok, giờ thì chuyển sang controller và tạo mã JWT

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
1

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

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
2

Kết quả thu được mã access toke giống như thế này

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
3

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ở

app.post('/users', [
   UsersController.insert
]);
1

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
4

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:

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
5

Công việc cuối cùng là thêm authentication middleware vào route

const userSchema = new Schema({
   firstName: String,
   lastName: String,
   email: String,
   password: String,
   permissionLevel: Number
});
6

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

Hướng dẫn secure nodejs api - api nodejs an toàn