Node.js + MongoDB: Authentication và Authorization sử dụng JWT

Tram Ho

I. Giới thiệu

Hầu hết hệ thống ứng dụng back-end, tính năng xác thực và phân quyền người dùng đều phải có. Ví dụ, khi tạo một website, đương nhiên bạn cần phải xây dựng tính năng đăng ký, đăng nhập, phân quyền admin, mod, member… Có một một số kỹ thuật giúp bạn xây dựng tính năng này, ví dụ: dùng Sessions, hoặc mới hơn là JWT.

Qua bài viết này, chúng ta sẽ cùng nhau xây dựng một ví dụ ứng dụng Node.js + MongoDB hỗ trợ tính năng User Authentication (đăng ký, đăng nhập) và Authorization bằng JSONWebToken (JWT).

II. Sự khác nhau “Authentication” và “Authorization”

Nghe hai thuật ngữ này có vẻ giống nhau đúng không? Tuy nhiên, chúng hơi khác một chút. Mình sẽ không đi sâu vào chi tiết hoạt động của chúng. Phần này, mình chỉ muốn làm nổi bật đặc điểm để bạn phân biệt Authentication và Authorization.

1.1. Authentication

Authentication là quá trình hệ thống kiểm tra, xác định danh tính của người dùng hoặc một hệ thống khác đang truy cập vào hệ thống hiện tại.

Hiểu nôm na, quá trình Authentication đi tìm câu trả lời cho câu hỏi: “Bạn là ai?”

Quá trình Authentication rất thông dụng, hầu hết các CMS liên quan tới quản lý nội dung, tương tác với người dùng đều có. Hiện nay, authentication xác thực chủ yếu dựa trên hai thông tin: tên người dùng và mật khẩu.

1.2. Authorization

Tương tự, quá trình authorization để trả lời cho câu hỏi: “Bạn được phép làm gì?“.

Về mặt kỹ thuật, quá trình authorization thường được thực hiện sau khi quá trình authentication kết thức. Tức là, sau khi biết bạn là ai rồi thì bước tiếp theo xác định bạn được phép làm gì trong hệ thống.

III. Token Based Authentication

So với kỹ thuật xác thực dựa trên Session, bạn cần phải lưu Session vào Cookie. Lợi thế lớn nhất của Token-base authentication là lưu JSON Web Token (JWT) trên client như: Local Storage trên Browser, Keychain trong iOS app hay SharedPreferences trong ứng dụng Android, .v.v…

Vì vậy, chúng ta không cần phải xây dựng một dự án vệ tinh hoặc một module xác thực bổ sung để hỗ trợ cho ứng dụng không dùng trên trình duyệt (ví dụ như các ứng dụng mobile Android, iOS…)

Dưới đây là sơ đồ luồng hoạt động của JWT.

Có 3 thành phần quan trọng của JWT:

  • Header
  • Payload
  • Signature

Chúng được kết hợp với nhau tạo thành một cấu trúc tiêu chuẩn:

Ứng dụng Client thường đính kèm mã JWT vào header với tiền tố Bearer:

Hoặc chỉ cần thêm một trường x-access-token trong header

IV. Thực hành Node.js & MongoDB User Authentication

Sau khi tìm hiểu xong lý thuyết, giờ là lúc bắt tay vào thực hành. Chúng ta sẽ xây dựng một ứng dụng Node.js + Express với tính năng user authentication + authorization, trong đó:

  • Người dùng có thể đăng ký tài khoản mới hoặc đăng nhập nếu đã có tài khoản.
  • Phân quyền tài khoản người dùng theo role (admin, moderator, user). Với mỗi role, người dùng có quyền khác nhau để truy cập vào tài nguyên.

Đây là danh sách những APIs cần thiết:

V. Flow chương trình cho tính năng Signup & Login

Dưới đây là diagram miêu tả quy trình mà ứng dụng Node.js sẽ thực hiện cho các tính năng Authentication (User Registration, User Login) và Authorization (phân quyền).

Một JWT hợp lệ để truy cập vào tài nguyên hệ thống phải có trường x-access-token trong Header của HTTP.

VI. Node.js Express Architecture cho Authentication & Authorization

Hình dưới đây mô tả tổng quan kiến trúc ứng dụng sử dụng Node.js + Express cho authentication & authorization.

Thông qua Express, các HTTP request hợp lệ và đúng với route đã thiết kế (xem lại bảng 3.1- danh sách các API sử dụng trong app) sẽ được kiểm tra bởi CORS Middleware trước khi vào Security layer.

Security layer bao gồm:

  • JWT Authentication Middleware: có nhiệm vụ xác minh SignUp, chuỗi token.
  • Authorization Middleware: Kiểm tra role của người dùng đăng nhập với các thông tin được lưu trong cơ sở dữ liệu.

Nếu gặp bất kỳ lỗi nào trong toàn bộ quá trình trên sẽ lập tức phản hồi lại cho client dưới dạng HTTP response (error code).

Các kỹ thuật được sử dụng ví dụ này (phiên bản có thể khác trong tương lai nhưng chắc không vấn đề gì đâu):

  • Express 4.17.1
  • bcryptjs 2.4.3
  • jsonwebtoken 8.5.1
  • mongoose 5.9.1
  • MongoDB

Yêu cầu môi trường phát triển, bạn cần phải cài đặt trước những phần mềm sau:

  • Nodejs: Hướng dẫn cài đặt Node + Npm chi tiết
  • MongoDB: đây là phần mềm quản trị cơ sở dữ liệu.
  • Tải và cài đặt Visual code hoặc Sublime Text 3: dùng để viết code nhanh hơn

VII. Cấu trúc thư mục dự án

Dưới đây là cấu trúc thư mục mã nguồn của dự án trong bài viết này:

VIII. Tạo dự án NodeJS

Để bắt đầu, chúng ta cần tạo mới một dự án NodeJS. Phần này thì mình sẽ không hướng dẫn lại nữa, bạn có thể tham khảo cách làm chi tiết tại đây: Tạo dự án NodeJS

Khi tạo dự án mới xong, bạn cần tạo thêm các thư viện cần thiết: express, cors, body-parser, mongoose, jsonwebtoken và bcryptjs.

Sử dụng npm để cài đặt chúng, gõ lệnh sau:

Nội dung package.json của dự án như sau:

IX. Thiết lập Express web server

Trong thư mục gốc của dự án, tạo thêm tệp server.js có nội dung như sau:

Mình sẽ giải thích một chút về đoạn code trong server.js:

  • Chúng ta import Express để tạo REST API
  • Thư viện body-parser được dùng để parse các request vào body object.
  • Import thư viện cors cung cấp Express middleware dùng để bật tính năng CORS

Cuối cùng, bạn có thể chạy thử ứng dụng bằng lệnh: npm start

Truy cập vào trình duyệt theo đường dẫn: http://localhost:8080/

X. Cấu hình kết nối MongoDB

Trong thư mục app, tạo riêng một thư mục mới, đặt tên là config. Thư mục này sẽ chứa tất cả các tệp liên quan tới cấu hình ứng dụng.

Trong thư mục config, bạn tạo tệp db.config.js để thêm các thông tin cài đặt cơ sở dữ liệu MongoDB cho ứng dụng:

XI. Định nghĩa Mongoose Model

Trong thư mục model, bạn tạo User và Role model như sau:

models/role.model.js

models/user.model.js

Các Mongoose Models này sẽ đại diện cho collection được tạo trong MongoDB. Khi bạn chạy chương trình, Mongoose sẽ tự động tạo hai collections có tên là: users và roles.

Sau khi đã khai báo xong, bạn không cần thiết phải tạo các hàm CRUD (đọc ghi cơ sở dữ liệu) vì Mongoose đã hỗ trợ sẵn rồi. Ví dụ:

  • Tạo mới một User: có hàm object.save()
  • Tìm một User theo Id: Sử dụng User.findById(id)
  • Tìm User theo email: User.findOne({ email: … })
  • Tìm tất cả roles: Role.find({…})

Những chức năng sẽ được chúng ta sử dụng trong Controllers. Cứ bình tĩnh nhé.

XII. Khởi tạo Mongoose

Bây giờ chúng ta tạo app/models/index.js với nội dung sau:

Mở lại tệp server.js để thêm đoạn mã sau để mở kết nối Mongoose với cơ sở dữ liệu MongoDB.

Hàm initial() cho phép chúng ta thêm dữ liệu 3 roles vào trong cơ sở dữ liệu, nếu trong DB có rồi thì bỏ qua.

XIII. Cấu hình Auth Key

Các hàm jsonwebtoken như: verify(), sign() sẽ cần tới một secret key để encode hay decode chuỗi token.

Trong thư mục app/config, tạo thêm auth.config.js với nội dung sau:

Trong đó, bạn có thể tạo chuối secret bất kỳ cho riêng bạn.

XIV. Tạo các hàm middleware

Quá trình để verify một hành động trong SignUp, chúng ta cần làm 2 việc:

Kiểm tra xem tên người dùng, email có bị trùng lặp trong DB hay không?
Kiểm tra xem role đăng ký có hợp lệ hay không?
middlewares/verifySignUp.js

Để xử lý việc Authentication & Authorization, chúng ta cần tạo các hàm sau:

  • Kiểm tra token có hợp lệ hay không? Chúng ta có thể lấy thông tin token trong trường x-access-token của Header HTTP, sau đó chuyển cho hàm verify() xử lý.
  • Kiểm tra role đăng ký đã có role chưa hay là trống?

middlewares/authJwt.js

Cuối cùng là tạo tệp index.js trong thư mục middlewares để export chúng:

XV. Tạo Controllers

Chúng ta sẽ lần lượt tạo controller cho 2 phần: Authentication và Authorization.

Controller cho Authentication
Với phần này, chúng ta có 2 công việc chính cho tính năng authentication:

  • Đăng ký: tạo người dùng mới và lưu trong cơ sở dữ liệu (với role mặc định là User nếu không chỉ định trước lúc đăng ký).
  • Đăng nhập: quá trình đăng nhập gồm 4 bước:
    • Tìm username trong cơ sở dữ liệu,
    • Nếu username tồn tại, so sánh password với password trong CSDL sử dụng. Nếu password khớp, tạo token bằng jsonwebtoken rồi trả về client với thông tin User kèm access-Token
      Nguyên lý chỉ có như vậy, giờ là mã nguồn:

controllers/auth.controller.js

Controller cho Authorization
Chúng ta có 4 APIs chính cho việc phân quyền:

  • /api/test/all
  • /api/test/user
  • /api/test/mod
  • /api/test/admin

controllers/user.controller.js

Phần tiếp theo, chúng ta sẽ kết hợp các controller này với middleware. Mọi người cảm thấy mỏi thì nghỉ ngơi, làm cốc cafe để lấy sức tiếp tục nhé.

XVI. Định nghĩa Routes

Khi một client gửi request tới web server sử dụng HTTP (GET, POST, PUT, DELETE) , chúng ta cần định nghĩa, xác định cách server tiếp nhận và phản hồi như thế nào. Đây chính là công dụng của các route.

Chúng ta chia các routes thành 2 nhóm: Authentication và Authorization

Authentication:

  • POST /api/auth/signup
  • POST /api/auth/signin
  • routes/auth.routes.js

Đừng quên thêm các routes vào trong tệp server.js

Như vậy là chúng ta đã hoàn thành dự án Node.js với Authentication rồi đấy. Bạn có thể tải toàn bộ mã nguồn trong bài viết tại đây:

Phần tiếp theo, chúng ta sẽ tiến hành chạy và test thử chương trình nhé.

XVII. Run & Test chương trình

Để chạy chương trình, bạn chỉ cần gõ lệnh: npm start

Sau khi chương trình chạy, chúng sẽ tự động tạo và thêm 3 roles cần thiết vào DB. Sử dụng Robo3T để xem dữ liệu trong MongoDB.

Có nhiều ứng dụng để test REST API, mình hay dùng Postman. Chúng ta test thử vớt API đăng ký mới: POST /api/auth/signup. Các API khác, các bạn tự thử nhé.

XVIII. Thay lời kết

Bài viết đến đây là kết thúc, chúng ta đã khám phá và thực hiện xây dựng ứng dụng Node.js để Authentication và Authorization sử dụng JWT (JSONWebToken).

Mình hi vọng bài viết này sẽ có ích cho bạn. Nếu có thắc mắc hãy để lại bình luận bên dưới nhé.

💦 Nguồn tham khảo:

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo