Xác thực Node.js với Twilio Verify

theanh

Administrator
Nhân viên
Xây dựng xác thực vào một ứng dụng là một nhiệm vụ tẻ nhạt. Tuy nhiên, đảm bảo xác thực này là không thể phá vỡ thậm chí còn khó hơn. Là nhà phát triển, chúng ta không thể kiểm soát được người dùng làm gì với mật khẩu của họ, cách họ bảo vệ mật khẩu, họ cung cấp mật khẩu cho ai hoặc cách họ tạo mật khẩu. Tất cả những gì chúng ta có thể làm là đến đủ gần để đảm bảo rằng yêu cầu xác thực được thực hiện bởi người dùng của chúng ta chứ không phải người khác. OTP chắc chắn giúp ích cho việc đó và các dịch vụ như Twilio Verify giúp chúng ta tạo OTP an toàn nhanh chóng mà không cần phải bận tâm đến logic.

Mật khẩu có vấn đề gì?​

Các nhà phát triển phải đối mặt với một số vấn đề khi chỉ sử dụng xác thực dựa trên mật khẩu vì nó có các vấn đề sau:
  1. Người dùng có thể quên mật khẩu và ghi lại (khiến mật khẩu có thể bị đánh cắp);
  2. Người dùng có thể sử dụng lại mật khẩu trên nhiều dịch vụ (khiến tất cả tài khoản của họ dễ bị xâm phạm dữ liệu một lần);
  3. Người dùng có thể sử dụng mật khẩu dễ nhớ, khiến chúng tương đối dễ bị hack.

Nhập OTP​

Mật khẩu dùng một lần (OTP) là mật khẩu hoặc mã PIN chỉ có hiệu lực cho một phiên đăng nhập hoặc giao dịch. Một khi nó chỉ có thể được sử dụng một lần, tôi chắc chắn bạn có thể thấy cách sử dụng OTP bù đắp cho những thiếu sót của mật khẩu truyền thống.

OTP bổ sung thêm một lớp bảo mật cho các ứng dụng mà hệ thống xác thực mật khẩu truyền thống không thể cung cấp. OTP được tạo ngẫu nhiên và chỉ có hiệu lực trong một thời gian ngắn, tránh được một số thiếu sót liên quan đến xác thực dựa trên mật khẩu truyền thống.

OTP có thể được sử dụng để thay thế mật khẩu truyền thống hoặc củng cố mật khẩu bằng cách sử dụng phương pháp xác thực hai yếu tố (2FA). Về cơ bản, OTP có thể được sử dụng ở bất cứ đâu bạn cần để đảm bảo danh tính của người dùng bằng cách dựa vào phương tiện truyền thông cá nhân do người dùng sở hữu, chẳng hạn như điện thoại, email, v.v.

Bài viết này dành cho các nhà phát triển muốn tìm hiểu về:
  1. Tìm hiểu cách xây dựng ứng dụng express.js đầy đủ;
  2. Triển khai xác thực bằng passport.js;
  3. Cách Twilio Verify để xác minh người dùng qua điện thoại.
Để đạt được các mục tiêu này, chúng tôi sẽ xây dựng ứng dụng đầy đủ bằng cách sử dụng node.js, express.js, EJS với xác thực được thực hiện bằng passport.js và các tuyến được bảo vệ yêu cầu OTP cho access.

Lưu ý: Tôi muốn đề cập rằng chúng tôi sẽ sử dụng một số gói của bên thứ 3 (do người khác xây dựng) trong ứng dụng của mình. Đây là một thông lệ phổ biến, vì không cần phải phát minh lại bánh xe. Chúng tôi có thể tạo máy chủ nút của riêng mình không? Có, tất nhiên rồi. Tuy nhiên, thời gian đó có thể được sử dụng tốt hơn vào việc xây dựng logic dành riêng cho ứng dụng của chúng tôi.

Mục lục​

  1. Tổng quan cơ bản về Xác thực trong các ứng dụng web;
  2. Xây dựng máy chủ Express;
  3. Tích hợp MongoDB vào ứng dụng Express của chúng tôi;
  4. Xây dựng chế độ xem của ứng dụng của chúng tôi bằng công cụ tạo mẫu EJS;
  5. Xác thực cơ bản bằng số hộ chiếu;
  6. Sử dụng Twilio Verify để bảo vệ các tuyến đường.

Yêu cầu​

  • Node.js
  • MongoDB
  • Trình soạn thảo văn bản (ví dụ: VS Code)
  • Trình duyệt web (ví dụ: Chrome, Firefox)
  • Hiểu biết về HTML, CSS, JavaScript, Express.js
Mặc dù chúng tôi sẽ xây dựng toàn bộ ứng dụng từ đầu, đây là Kho lưu trữ GitHub cho dự án.

Tổng quan cơ bản về xác thực trong ứng dụng web​

Xác thực là gì?​

Xác thực là toàn bộ quá trình xác định người dùng và xác minh rằng người dùng có tài khoản trên ứng dụng của chúng tôi.
Xác thực không nên nhầm lẫn với ủy quyền. Mặc dù chúng hoạt động song song với nhau, nhưng không có quyền hạn nào mà không có xác thực.
Nói như vậy, chúng ta hãy xem quyền hạn là gì.

Quyền hạn là gì?​

Quyền hạn về cơ bản nhất là về quyền của người dùng — những gì người dùng được phép làm trong ứng dụng. Nói cách khác:
  1. Xác thực: Bạn là ai?
  2. Quyền hạn: Bạn có thể làm gì?
Xác thực diễn ra trước Quyền hạn.
Không có Quyền hạn nào mà không có Xác thực.
Cách xác thực người dùng phổ biến nhất là thông qua tên người dùngmật khẩu.

Thiết lập ứng dụng của chúng tôi​

Để thiết lập ứng dụng, chúng tôi tạo thư mục dự án:
Mã:
mkdir authWithTwilioVerify

Xây dựng máy chủ Express​

Chúng tôi sẽ sử dụng Express.js để xây dựng máy chủ của mình.

Tại sao chúng ta cần Express?​

Xây dựng máy chủ trong Node có thể rất tẻ nhạt, nhưng các khuôn khổ giúp chúng ta thực hiện mọi việc dễ dàng hơn.Express là khuôn khổ web Node phổ biến nhất. Nó cho phép chúng ta:
  • Viết trình xử lý cho các yêu cầu có các động từ HTTP khác nhau tại các đường dẫn URL khác nhau (tuyến đường);
  • Tích hợp với các công cụ kết xuất view để tạo phản hồi bằng cách chèn dữ liệu vào các mẫu;
  • Đặt các thiết lập ứng dụng web chung — như cổng được sử dụng để kết nối và vị trí của các mẫu được sử dụng để kết xuất phản hồi;
  • Thêm phần mềm trung gian xử lý yêu cầu bổ sung tại bất kỳ điểm nào trong đường ống xử lý yêu cầu.
Ngoài tất cả những điều này, các nhà phát triển đã tạo ra các gói phần mềm trung gian tương thích để giải quyết hầu hết mọi vấn đề phát triển web.

Trong thư mục authWithTwilioVerify của chúng tôi, chúng tôi khởi tạo package.json chứa thông tin liên quan đến dự án của chúng tôi.
Mã:
cd authWithTwilioVerifynpm init -y
Để duy trì kiến trúc Model View Controller (MVC), chúng ta phải tạo các thư mục sau trong thư mục authWithTwilioVerify của mình:
Mã:
mkdir public controllers views route config models
Nhiều nhà phát triển có những lý do khác nhau để sử dụng kiến trúc MVC, nhưng đối với cá nhân tôi, đó là vì:
  1. Nó khuyến khích việc tách biệt các mối quan tâm;
  2. Nó giúp viết mã sạch;
  3. Nó cung cấp một cấu trúc cho cơ sở dữ liệu của tôi và vì các nhà phát triển khác cũng sử dụng nó nên việc hiểu cơ sở dữ liệu sẽ không thành vấn đề.
  • Controllers chứa các bộ điều khiển;
  • Models chứa các mô hình cơ sở dữ liệu của chúng ta;
  • Public chứa các tài sản tĩnh của chúng ta, ví dụ: Tệp CSS, hình ảnh, v.v.;
  • Thư mục Views chứa các trang sẽ được hiển thị trong trình duyệt;
  • Thư mục Routes chứa các tuyến đường khác nhau của ứng dụng;
  • Thư mục Config chứa thông tin đặc thù của ứng dụng.
Chúng ta cần cài đặt các gói sau để xây dựng ứng dụng:
  • nodemon tự động khởi động lại máy chủ khi chúng ta thực hiện thay đổi;
  • express cung cấp cho chúng ta giao diện đẹp để xử lý các tuyến đường;
  • express-session cho phép chúng ta xử lý các phiên dễ dàng trong ứng dụng express của mình;
  • connect-flash cho phép chúng ta hiển thị thông báo cho người dùng.
Mã:
npm install nodemon -D
Thêm tập lệnh bên dưới vào tệp package.json để khởi động máy chủ của chúng ta bằng nodemon.
Mã:
"scripts": { "dev": "nodemon index" },
Mã:
npm install express express-session connect-flash --save
Tạo tệp index.js và thêm các gói cần thiết cho ứng dụng của chúng ta.

Chúng ta phải require các gói đã cài đặt vào tệp index.js của mình để ứng dụng chạy tốt, sau đó chúng ta định cấu hình các gói như sau:
Mã:
const path = require('path')const express = require('express');const session = require('express-session')const flash = require('connect-flash')const port = process.env.PORT || 3000const app = express();app.use('/static', express.static(path.join(__dirname, 'public')))app.use(session({ secret: "please log me in", resave: true, saveUninitialized: true }));app.use(express.json())app.use(express.urlencoded({ extended: true }))// Kết nối flashapp.use(flash());// Biến toàn cụcapp.use(function(req, res, next) { res.locals.success_msg = req.flash('success_msg'); res.locals.error_msg = req.flash('error_msg'); res.locals.error = req.flash('error'); res.locals.error = req.flash('error'); res.locals.user = req.user next();});//define error handlerapp.use(function(err, req, res, next) { res.render('error', { error : err })})//listen on portapp.listen(port, () => { console.log(`app is running on port ${port}`)});
Chúng ta hãy phân tích đoạn mã trên.

Ngoài các câu lệnh require, chúng ta sử dụng hàm app.use() — cho phép chúng ta sử dụng middleware cấp ứng dụng.

Các hàm trung gian là các hàm có quyền truy cập vào đối tượng yêu cầu, đối tượng phản hồi và hàm trung gian tiếp theo trong chu kỳ yêu cầu và phản hồi của ứng dụng.
Hầu hết các gói có quyền truy cập vào trạng thái của ứng dụng (đối tượng yêu cầu và phản hồi) và có thể thay đổi các trạng thái đó thường được sử dụng làm phần mềm trung gian. Về cơ bản, phần mềm trung gian bổ sung chức năng cho ứng dụng express của chúng ta.
Giống như việc chuyển trạng thái ứng dụng cho hàm phần mềm trung gian, nói rằng đây là trạng thái, bạn muốn làm gì với nó và gọi hàm next() cho phần mềm trung gian tiếp theo.

Cuối cùng, chúng ta yêu cầu máy chủ ứng dụng của mình lắng nghe các yêu cầu đến cổng 3000.

Sau đó, trong thiết bị đầu cuối, hãy chạy:
Mã:
npm run dev
Nếu bạn thấy ứng dụng đang chạy trên cổng 3000 trong thiết bị đầu cuối, điều đó có nghĩa là ứng dụng của chúng ta đang chạy bình thường.

Tích hợp MongoDB vào ứng dụng Express của chúng ta​

MongoDB lưu trữ dữ liệu dưới dạng tài liệu. Các tài liệu này được lưu trữ trong MongoDB theo định dạng JSON (Ký hiệu đối tượng JavaScript). Vì chúng ta đang sử dụng Node.js, nên việc chuyển đổi dữ liệu được lưu trữ trong MongoDB thành các đối tượng JavaScript và thao tác chúng khá dễ dàng.

Để cài đặt MongoDB trên máy của bạn, hãy truy cập tài liệu của MongoDB.

Để tích hợp MongoDB vào ứng dụng express của mình, chúng ta sẽ sử dụng Mongoose. Mongoose là một ODM (viết tắt của object data mapper).

Về cơ bản, Mongoose giúp chúng ta sử dụng MongoDB trong ứng dụng dễ dàng hơn bằng cách tạo một trình bao bọc xung quanh các hàm MongoDB gốc.
Mã:
npm install mongoose --save
Trong index.js, nó yêu cầu mongoose:
Mã:
const mongoose = require('mongoose')const app = express()//kết nối với mongodbmongoose.connect('mongodb://localhost:27017/authWithTwilio',{ useNewUrlParser: true, useUnifiedTopology: true}).then(() => { console.log(`đã kết nối với mongodb`)}).catch(e => console.log(e))
Hàm mongoose.connect() cho phép chúng ta thiết lập kết nối tới cơ sở dữ liệu MongoDB của mình bằng chuỗi kết nối.

Định dạng cho chuỗi kết nối là mongodb://localhost:27017/{database_name}.

mongodb://localhost:27017/ là máy chủ mặc định của MongoDB và database_name là bất kỳ tên nào chúng ta muốn gọi cơ sở dữ liệu của mình.

Mongoose kết nối tới cơ sở dữ liệu có tên là database_name. Nếu không tồn tại, nó sẽ tạo một cơ sở dữ liệu với database_name và kết nối với cơ sở dữ liệu đó.

Mongoose.connect() là một lời hứa, vì vậy, việc ghi nhật ký thông báo vào bảng điều khiển trong các phương thức then()catch() để cho chúng ta biết kết nối có thành công hay không luôn là một cách làm tốt.

Chúng ta tạo mô hình người dùng trong thư mục models của mình:
Mã:
cd modelstouch user.js
user.js yêu cầu mongoose và tạo lược đồ người dùng của chúng ta:
Mã:
const mongoose = require('mongoose');const userSchema = new mongoose.Schema({ name : { type: String, required: true }, username : { type: String, required: true }, password : { type: String, required: true }, phonenumber : { type: String, required: true }, email : { type: String, required: true }, verified: Boolean})module.exports = mongoose.model('user', userSchema)
Một schema cung cấp một cấu trúc cho dữ liệu của chúng ta. Nó cho thấy cách dữ liệu nên được cấu trúc trong cơ sở dữ liệu. Theo đoạn mã trên, chúng tôi chỉ định rằng một đối tượng người dùng trong cơ sở dữ liệu phải luôn có name, username, password, phonenumberemail. Vì những trường đó là bắt buộc, nếu dữ liệu được đẩy vào cơ sở dữ liệu thiếu bất kỳ trường bắt buộc nào trong số này, mongoose sẽ báo lỗi.

Mặc dù bạn có thể tạo dữ liệu không có lược đồ trong MongoDB, nhưng không nên làm như vậy — tin tôi đi, dữ liệu của bạn sẽ rất lộn xộn. Bên cạnh đó, lược đồ rất tuyệt. Chúng cho phép bạn chỉ định cấu trúc và hình thức của các đối tượng trong cơ sở dữ liệu của mình — ai lại không muốn có những quyền hạn như vậy chứ?

Mã hóa mật khẩu​

Cảnh báo: không bao giờ lưu trữ mật khẩu của người dùng dưới dạng văn bản thuần túy trong cơ sở dữ liệu của bạn.
Luôn mã hóa mật khẩu trước khi đẩy chúng vào cơ sở dữ liệu.
Lý do chúng ta cần mã hóa mật khẩu người dùng là: trong trường hợp ai đó bằng cách nào đó truy cập vào cơ sở dữ liệu của chúng ta, chúng ta có thể đảm bảo rằng mật khẩu người dùng được an toàn — vì tất cả những gì người này thấy sẽ chỉ là một băm. Điều này cung cấp một số mức độ đảm bảo an ninh, nhưng một hacker tinh vi vẫn có thể bẻ khóa băm này nếu họ có các công cụ phù hợp. Do đó cần có OTP, nhưng chúng ta hãy tập trung vào việc mã hóa mật khẩu người dùng ngay bây giờ.

bcryptjs cung cấp một cách để mã hóa và giải mã mật khẩu của người dùng.
Mã:
npm install bcryptjs
Trong models/user.js, nó yêu cầu bcryptjs:
Mã:
//sau khi yêu cầu mongooseconst bcrypt = require('bcryptjs')//trước module.exports//băm mật khẩu khi lưuuserSchema.pre('save', async function() { return new Promise( async (resolve, reject) => { await bcrypt.genSalt(10, async (err, salt) => { await bcrypt.hash(this.password, salt, async (err, hash) => { if(err) { reject (err) } else { resolve (this.password = hash) } }); }); })})userSchema.methods.validPassword = async function(password) { return new Promise((resolve, reject) => { bcrypt.compare(password, this.password, (err, res) => { if(err) { reject (err) } resolve (res) }); })}
Đoạn mã trên thực hiện một số việc. Chúng ta hãy cùng xem chúng.

userSchema.pre('save', callback)móc mongoose cho phép chúng ta thao tác dữ liệu trước khi lưu vào cơ sở dữ liệu. Trong hàm gọi lại, chúng ta trả về một lời hứa cố gắng băm(encrypt) bcrypt.hash() mật khẩu bằng cách sử dụng bcrypt.genSalt() mà chúng ta đã tạo. Nếu xảy ra lỗi trong quá trình băm này, chúng ta từ chối hoặc giải quyết bằng cách đặt this.password = hash. this.passworduserSchema password.

Tiếp theo, mongoose cung cấp cho chúng ta một cách để thêm các phương thức vào lược đồ bằng cách sử dụng schema.methods.method_name. Trong trường hợp của chúng ta, chúng ta đang tạo một phương thức cho phép chúng ta xác thực mật khẩu người dùng. Gán giá trị hàm cho *userSchema.methods.validPassword*, chúng ta có thể dễ dàng sử dụng phương thức so sánh bcryptjs bcryprt.compare() để kiểm tra xem mật khẩu có đúng hay không.

bcrypt.compare() lấy hai đối số và một lệnh gọi lại. password là mật khẩu được truyền khi gọi hàm, trong khi this.password là mật khẩu từ userSchema.
Tôi thích phương pháp xác thực mật khẩu của người dùng này vì nó giống như một thuộc tính trên đối tượng người dùng. Người ta có thể dễ dàng gọi User.validPassword(password) và nhận được true hoặc false làm phản hồi.
Hy vọng bạn có thể thấy được tính hữu ích của mongoose. Bên cạnh việc tạo ra một lược đồ cung cấp cấu trúc cho các đối tượng cơ sở dữ liệu của chúng ta, nó cũng cung cấp các phương thức hay để thao tác các đối tượng đó — nếu không thì sẽ khá rắc rối khi chỉ sử dụng MongoDB gốc.
Express đối với Node, cũng như Mongoose đối với MongoDB.

Xây dựng các chế độ xem của ứng dụng bằng EJS Templating Engine​

Trước khi bắt đầu xây dựng các chế độ xem của ứng dụng, chúng ta hãy xem qua kiến trúc giao diện người dùng của ứng dụng.

Kiến trúc giao diện người dùng​

EJS là một công cụ tạo mẫu hoạt động trực tiếp với Express. Không cần một khuôn khổ giao diện người dùng khác. EJS giúp việc truyền dữ liệu trở nên rất dễ dàng. Nó cũng giúp theo dõi những gì đang diễn ra dễ dàng hơn vì không cần chuyển đổi từ back-end sang front-end.

Chúng ta sẽ có một thư mục views, chứa các tệp sẽ được hiển thị trong trình duyệt. Tất cả những gì chúng ta phải làm là gọi phương thức res.render() từ bộ điều khiển của mình. Ví dụ, nếu chúng ta muốn hiển thị trang đăng nhập, chỉ cần gọi res.render('login'). Chúng ta cũng có thể truyền dữ liệu cho các chế độ xem bằng cách thêm một đối số bổ sung — là một đối tượng cho phương thức render(), như res.render('dashboard', { user }). Sau đó, trong view của mình, chúng ta có thể hiển thị dữ liệu bằng cú pháp đánh giá . Mọi thứ có thẻ này đều được đánh giá — ví dụ, hiển thị giá trị của thuộc tính username của đối tượng người dùng. Ngoài cú pháp đánh giá, EJS còn cung cấp cú pháp điều khiển (), cho phép chúng ta viết các câu lệnh điều khiển chương trình như điều kiện, vòng lặp, v.v.

Về cơ bản, EJS cho phép chúng ta nhúng JavaScript vào HTML của mình.
Mã:
npm install ejs express-ejs-layouts --save
Trong index.js, nó yêu cầu express-ejs-layouts:
Mã:
//sau khi yêu cầu connect-flashconst expressLayouts = require('express-ejs-layouts')//sau logic mongoose.connectapp.use(expressLayouts);app.set('view engine', 'ejs');
Sau đó:
Mã:
cd viewstouch layout.ejs
Trong views/layout.ejs,
Mã:
[*]     Xác thực Node js
Tệp layout.ejs đóng vai trò như một tệp index.html, nơi chúng ta có thể đưa vào tất cả các tập lệnh và bảng định kiểu của mình. Sau đó, trong div với các lớp ui container, chúng ta sẽ render body — là phần còn lại của các chế độ xem ứng dụng của chúng ta.

Chúng ta sẽ sử dụng Semantic UI làm khung CSS của chúng ta.

Xây dựng các thành phần​

Các thành phần là nơi chúng ta lưu trữ mã có thể sử dụng lại, do đó chúng ta không phải viết lại chúng mỗi lần. Tất cả những gì chúng ta làm là bao gồm chúng ở bất cứ nơi nào cần thiết.
Bạn có thể nghĩ về các thành phần giống như các thành phần trong các khung giao diện người dùng: chúng khuyến khích mã DRY và khả năng tái sử dụng mã. Hãy nghĩ về partials như một phiên bản trước đó của các thành phần.
Ví dụ, chúng ta muốn partials cho menu của mình, để chúng ta không phải viết mã cho nó mỗi lần chúng ta cần menu trên trang của mình.
Mã:
cd viewsmkdir partials
Chúng ta sẽ tạo hai tệp trong thư mục /views/partials:
Mã:
cd partialstouch menu.ejs message.ejs
Trong menu.ejs,
Mã:
  Trang chủ    bảng điều khiển       Đăng xuất      Đăng ký   Đăng nhập
Trong message.ejs,
Mã:
   [I][/I]  Đăng ký người dùng không thành công      [I][/I]  Đăng ký người dùng của bạn đã thành công.    [I][/I]     [I][/I]

Xây dựng Trang Bảng điều khiển​

Trong thư mục chế độ xem của chúng tôi, chúng tôi tạo tệp dashboard.ejs:
Mã:
[HEADING=1] DashBoard[/HEADING]
Tại đây, chúng tôi bao gồm menu partials để chúng tôi có menu trên trang.

Xây dựng trang lỗi​

Trong thư mục chế độ xem của chúng tôi, chúng tôi tạo tệp error.ejs:
Mã:
[HEADING=1]Trang lỗi[/HEADING]

Xây dựng Trang chủ​

Trong thư mục chế độ xem của chúng tôi, chúng tôi tạo tệp home.ejs:
Mã:
[HEADING=1] Chào mừng đến với Trang chủ[/HEADING]

Xây dựng Trang đăng nhập​

Trong thư mục chế độ xem, chúng tôi tạo tệp login.ejs:
Mã:
  [HEADING=3] Biểu mẫu đăng nhập [/HEADING]   Email    Password   Login

Xây dựng trang xác minh​

Trong thư mục chế độ xem của chúng tôi, chúng tôi tạo tệp login.ejs:
Mã:
[HEADING=1]Xác minh trang[/HEADING]
vui lòng xác minh tài khoản của bạn
  verification code   Verify
Gửi lại mã
Tại đây, chúng tôi cung cấp một biểu mẫu để người dùng nhập mã xác minh sẽ được gửi cho họ.

Xây dựng trang đăng ký​

Chúng tôi cần lấy số điện thoại di động của người dùng và chúng ta đều biết rằng mã quốc gia khác nhau tùy theo quốc gia. Do đó, chúng ta sẽ sử dụng [intl-tel-input](https://intl-tel-input.com/) để giúp chúng ta với mã quốc gia và xác thực số điện thoại.
Mã:
npm install intl-tel-input

  1. Trong thư mục public của chúng ta, chúng ta tạo một thư mục css, thư mục js và thư mục img:
    Mã:
    cd publicmkdir css js img

  2. Chúng ta sao chép tệp intlTelInput.css từ tệp node_modules\intl-tel-input\build\css\ vào thư mục public/css của chúng ta.

  3. Chúng ta sao chép cả intlTelInput.jsutils.js từ thư mục node_modules\intl-tel-input\build\js\ vào thư mục public/js của chúng tôi.

  4. Chúng tôi sao chép cả flags.png[email protected] từ thư mục node_modules\intl-tel-input\build\img\ vào thư mục public/img của chúng tôi.
Chúng tôi tạo một app.css trong thư mục public/css của chúng tôi:
Mã:
cd publictouch app.css
Trong app.css, thêm các kiểu bên dưới:
Mã:
.iti__flag {background-image: url("/static/img/flags.png");}@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .iti__flag {background-image: url("/static/img/[email protected]");}}.hide { display: none}.error { color: red; outline: 1px solid red;}.success{ color: green;}
Cuối cùng, chúng ta tạo một tệp signup.ejs trong thư mục chế độ xem của mình:
Mã:
  [HEADING=3] Biểu mẫu đăng ký [/HEADING]   Tên    Tên người dùng    Mật khẩu    Số điện thoại  ✓ Hợp lệ    Email   Đăng ký  const input = document.querySelector("#phone") const errorMsg = document.querySelector("#error-msg") const validMsg = document.querySelector("#valid-msg") const errorMap = ["Số không hợp lệ", "Mã quốc gia không hợp lệ", "Quá ngắn", "Quá dài", "Số không hợp lệ"]; const iti = window.intlTelInput(input, { separateDialCode: true, autoPlaceholder: "aggressive", hiddenInput: "phonenumber", utilsScript: "/static/js/utils.js?1590403638580" // chỉ để định dạng/giữ chỗ, v.v. }); var reset = function() { input.classList.remove("error"); errorMsg.innerHTML = ""; errorMsg.classList.add("hide"); validMsg.classList.add("hide"); }; // khi làm mờ: xác thực input.addEventListener('blur', function() { reset(); if (input.value.trim()) { if (iti.isValidNumber()) { validMsg.classList.remove("hide"); } else { input.classList.add("error"); var errorCode = iti.getValidationError(); errorMsg.innerHTML = errorMap[errorCode]; errorMsg.classList.remove("hide"); } } }); // khi keyup / thay đổi cờ: đặt lại input.addEventListener('change', reset); input.addEventListener('keyup', reset); document.querySelector('.ui.form').addEventListener('submit', (e) => { if(!iti.isValidNumber()){ e.preventDefault() } })


Xác thực cơ bản với Passport​

Việc xây dựng xác thực vào ứng dụng có thể thực sự phức tạp và tốn thời gian, vì vậy chúng ta cần một gói để giúp chúng ta thực hiện việc đó.
Hãy nhớ: đừng phát minh lại bánh xe, trừ khi ứng dụng của bạn có nhu cầu cụ thể.
passport là một gói giúp với xác thực trong ứng dụng express của chúng ta.

passport có nhiều chiến lược mà chúng ta có thể sử dụng, nhưng chúng ta sẽ sử dụng local-strategy — về cơ bản là thực hiện xác thực tên người dùng và mật khẩu.
Một lợi thế của việc sử dụng passport là vì nó có nhiều chiến lược, chúng ta có thể dễ dàng mở rộng ứng dụng của mình để sử dụng các chiến lược khác của nó.
Mã:
npm install passport passport-local
Trong index.js, chúng ta thêm mã sau:
Mã:
//sau khi yêu cầu expressconst passport = require('passport')//sau khi yêu cầu mongooseconst { localAuth } = require('./config/passportLogic')//sau khi const app = express()localAuth(passport)//sau khi app.use(express.urlencoded({ extended: true }))app.use(passport.initialize());app.use(passport.session());
Chúng tôi đang thêm một số phần mềm trung gian cấp ứng dụng vào tệp index.js của mình — cho biết ứng dụng sử dụng passport.initialize() và phần mềm trung gian passport.session().

Passport.initialize() khởi tạo passport, trong khi phần mềm trung gian passport.session() cho passport biết rằng chúng tôi đang sử dụng session để xác thực.

Đừng lo lắng nhiều về hàm localAuth(). Điều đó lấy đối tượng passport làm đối số và chúng ta sẽ tạo hàm ngay bên dưới.

Tiếp theo, chúng ta tạo thư mục config và tạo các tệp cần thiết:
Mã:
mkdir configtouch passportLogic.js middleware.js
Trong passportLogic.js,
Mã:
//tệp chứa logic hộ chiếu để đăng nhập cục bộconst LocalStrategy = require('passport-local').Strategy;const mongoose = require('mongoose')const User = require('../models/user')const localAuth = (passport) => { passport.use( new LocalStrategy( { usernameField: 'email' }, async(email, password, done) => { try { const user = await User.findOne({ email: email }) if (!user) { return done(null, false, { message: 'Email không đúng' }); } //xác thực mật khẩu const valid = await user.validPassword(password) if (!valid) { return done(null, false, { message: 'Mật khẩu không đúng.' }); } return done(null, user); } catch (lỗi) { return done(lỗi) } } )); passport.serializeUser(hàm(người dùng, xong) { xong(null, user.id); }); passport.deserializeUser(hàm(id, xong) { User.findById(id, hàm(err, user) { xong(err, user); }); });}module.exports = { localAuth}
Hãy cùng tìm hiểu những gì đang diễn ra trong đoạn mã trên.

Ngoài các câu lệnh require, chúng ta tạo hàm localAuth(), hàm này sẽ được xuất từ tệp. Trong hàm, chúng ta gọi hàm passport.use() sử dụng LocalStrategy() để xác thực dựa trên usernamepassword.

Chúng ta chỉ định rằng usernameField của chúng ta phải là email. Sau đó, chúng ta tìm một người dùng có email cụ thể đó — nếu không có email nào tồn tại, chúng ta sẽ trả về lỗi trong hàm done(). Tuy nhiên, nếu người dùng tồn tại, chúng ta sẽ kiểm tra xem mật khẩu có hợp lệ hay không bằng phương thức validPassword trên đối tượng User. Nếu mật khẩu không hợp lệ, chúng ta sẽ trả về lỗi. Cuối cùng, nếu mọi thứ thành công, chúng tôi trả về user trong done(null, user).

passport.serializeUser()passport.deserializeUser() giúp hỗ trợ các phiên đăng nhập. Passport sẽ tuần tự hóa và hủy tuần tự hóa các phiên bản user thànhtừ phiên.

Trong middleware.js,
Mã:
//kiểm tra xem người dùng đã được xác minh chưaconst isLoggedIn = async(req, res, next) => { if(req.user){ return next() } else { req.flash( 'error_msg', 'Bạn phải đăng nhập để thực hiện điều đó' ) res.redirect('/users/login') }}const notLoggedIn = async(req, res, next) => { if(!req.user) { return next() } else{ res.redirect('back') }}module.exports = { isLoggedIn, notLoggedIn}
Tệp phần mềm trung gian của chúng tôi chứa hai (2) phần mềm trung gian cấp tuyến đường, sẽ được sử dụng sau trong các tuyến đường của chúng tôi.
Phần mềm trung gian cấp tuyến đường được các tuyến đường của chúng tôi sử dụng, chủ yếu để bảo vệ và xác thực tuyến đường, chẳng hạn như ủy quyền, trong khi phần mềm trung gian cấp ứng dụng được toàn bộ ứng dụng sử dụng.
isLoggedInnotLoggedInphần mềm trung gian cấp tuyến đường kiểm tra xem người dùng đã đăng nhập hay chưa. Chúng tôi sử dụng các phần mềm trung gian này để chặn quyền truy cập vào các tuyến đường mà chúng tôi muốn người dùng đã đăng nhập có thể truy cập.

Xây dựng Bộ điều khiển đăng ký​

Mã:
cd controllersmkdir signUpController.js loginController.js
Trong signUpController.js, chúng tôi:
  1. Kiểm tra thông tin xác thực của người dùng;
  2. Kiểm tra xem người dùng có thông tin chi tiết đó (email hoặc số điện thoại) có tồn tại trong cơ sở dữ liệu của chúng tôi không;
  3. Tạo lỗi nếu người dùng đó tồn tại;
  4. Cuối cùng, nếu người dùng đó không tồn tại, chúng tôi tạo một người dùng mới với các thông tin chi tiết đã cho và chuyển hướng đến trang đăng nhập.
Mã:
const mongoose = require('mongoose')const User = require('../models/user')//log up Logicconst getSignup = async(req, res, next) => { res.render('signup')}const createUser = async (req, res, next) => { thử { const { tên, tên người dùng, mật khẩu, số điện thoại, email} = await req.body const lỗi = [] const reRenderSignup = (yêu cầu, res, tiếp theo) => { console.log(errors) res.render('signup', { errors, username, name, phonenumber, email }) } if( !name || !username || !password || !phonenumber || !email ) { errors.push({ msg: 'vui lòng điền đầy đủ thông tin vào tất cả các trường' }) reRenderSignup(req, res, next) } else { const existingUser = await User.findOne().or([{ email: email}, { phonenumber : phonenumber }]) if(existingUser) { errors.push({ msg: 'Người dùng đã tồn tại, hãy thử thay đổi email hoặc số điện thoại của bạn' }) reRenderSignup(req, res, next) } else { const user = await User.create( req.body ) req.flash( 'success_msg', 'Bạn đã đăng ký và có thể đăng nhập' ); res.redirect('/users/login') } } } catch (error) { next(error) }}module.exports = { createUser, getSignup}
Trong loginController.js,
  1. Chúng tôi sử dụng phương thức passport.authenticate() với phạm vi cục bộ (email và mật khẩu) để kiểm tra xem người dùng có tồn tại không;
  2. Nếu người dùng không tồn tại, chúng tôi đưa ra thông báo lỗi và chuyển hướng người dùng đến cùng một tuyến đường;
  3. Nếu người dùng tồn tại, chúng tôi đăng nhập người dùng bằng phương thức req.logIn, gửi cho họ xác minh bằng hàm sendVerification(), sau đó chuyển hướng họ đến tuyến đường verify.
Mã:
const mongoose = require('mongoose')const passport = require('passport')const User = require('../models/user')const { sendVerification } = require('../config/twilioLogic')const getLogin = async(req, res) => { res.render('đăng nhập')}const authUser = async(req, res, next) => { thử { hộ chiếu.xác thực('cục bộ', hàm(lỗi, người dùng, thông tin) { nếu (lỗi) { trả về tiếp theo(lỗi) } nếu (!người dùng) { req.flash( 'error_msg', thông tin.message ) trả về res.redirect('/người dùng/đăng nhập') } req.logIn(người dùng, hàm(lỗi) { nếu (lỗi) { trả về tiếp theo(lỗi) } sendVerification(req, res, req.user.phonenumber) res.redirect('/người dùng/xác minh'); }); })(req, res, tiếp theo); } catch (error) { next(error) }}module.exports = { getLogin, authUser}
Hiện tại, sendVerification() không thực sự hoạt động. Đó là vì chúng ta chưa viết hàm, vì vậy chúng ta cần Twilio cho việc đó. Hãy cài đặt Twilio và bắt đầu.

Sử dụng Twilio Verify để bảo vệ các tuyến đường​

Để sử dụng Twilio Verify, bạn:
  1. Truy cập https://www.twilio.com/;
  2. Tạo tài khoản với Twilio;
  3. Đăng nhập vào bảng điều khiển của bạn;
  4. Chọn tạo dự án mới;
  5. Thực hiện theo các bước để tạo dự án mới.
Để cài đặt Twilio SDK cho node.js:
Mã:
npm install twilio
Tiếp theo, chúng ta cần cài đặt dotenv để hỗ trợ chúng ta với biến môi trường.
Mã:
npm install dotenv
Chúng tôi tạo một tệp trong thư mục gốc của dự án và đặt tên là .env. Tệp này là nơi chúng tôi lưu trữ thông tin xác thực của mình, vì vậy chúng tôi không đẩy nó lên git. Để thực hiện điều đó, chúng tôi tạo một tệp .gitignore trong thư mục gốc của dự án và thêm các dòng sau vào tệp:
Mã:
node_modules.env
Điều này yêu cầu git bỏ qua cả thư mục node_modules và tệp .env.

Để lấy thông tin xác thực tài khoản Twilio, chúng tôi đăng nhập vào bảng điều khiển Twilio và sao chép ACCOUNT SIDAUTH TOKEN của mình. Sau đó, chúng ta nhấp vào lấy số dùng thử và Twilio sẽ tạo cho chúng ta một số dùng thử, nhấp vào chấp nhận số. Bây giờ từ bản sao bảng điều khiển, chúng ta sao chép số dùng thử của mình.

Trong .env,
TWILIO_ACCOUNT_SID = <YOUR_ACCOUNT_SID>
TWILIO_AUTH_TOKEN = <YOUR_AUTH_TOKEN>
TWILIO_PHONE_NUMBER = <TOUR_TWILIO_NUMBER>
Đừng quên thay thế , bằng thông tin xác thực thực tế của bạn.

Chúng ta tạo một tệp có tên twilioLogic.js trong config thư mục:
Mã:
cd cofigtouch twilioLogic.js
Trong twilioLogic.js,
Mã:
require('dotenv').config()const twilio = require('twilio')const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)//tạo dịch vụ xác minhconst createService = async(req, res) => { client.verify.services.create({ friendlyName: 'phoneVerification' }) .then(service => console.log(service.sid))}createService();
Trong đoạn mã trên, chúng ta tạo một dịch vụ verify mới.

Chạy:
Mã:
node config/twilioLogic.js
Chuỗi được ghi vào màn hình của chúng ta là TWILIO_VERIFICATION_SID — chúng ta sao chép chuỗi đó.

Trong .env, thêm dòng TWILIO_VERIFICATION_SID = .

Trong config/twilioLogic.js, chúng ta xóa dòng createService(), vì chúng ta chỉ cần tạo dịch vụ verify một lần. Sau đó, chúng ta thêm các dòng mã sau:
Mã:
//sau khi tạo hàm createService//gửi mã xác minhconst sendVerification = async(req, res, number) => { client.verify.services(process.env.TWILIO_VERIFICATION_SID) .verifications .create({to: `${number}`, channel: 'sms'}) .then( verification => console.log(verification.status) );}//kiểm tra mã xác minhconst checkVerification = async(req, res, number, code) => { return new Promise((resolve, reject) => { client.verify.services(process.env.TWILIO_VERIFICATION_SID) .verificationChecks .create({to: `${number}`, code: `${code}`}) .then(verification_check => { resolve(verification_check.status) }); })}module.exports = { sendVerification, checkVerification}
sendVerification là một hàm bất đồng bộ trả về một lời hứa gửi OTP xác minh đến số được cung cấp bằng kênh sms.

checkVerification cũng là một hàm bất đồng bộ trả về một lời hứa kiểm tra trạng thái xác minh. Nó kiểm tra xem OTP do người dùng cung cấp có phải là OTP giống với OTP đã được gửi cho họ hay không.

Trong config/middleware.js, hãy thêm nội dung sau:
Mã:
//sau khi khai báo hàm notLoggedIn//ngăn người dùng chưa xác minh truy cập vào '/dashboard'const isVerified = async(req, res, next) => { if(req.session.verified){ return next() } else { req.flash( 'error_msg', 'Bạn phải được xác minh để thực hiện điều đó' ) res.redirect('/users/login') }}//ngăn Người dùng đã xác minh truy cập vào '/verify'const notVerified = async(req, res, next) => { if(!req.session.verified){ return next() } else { res.redirect('back') }}module.exports = { //after notLoggedIn isVerified, notVerified}
Chúng tôi đã tạo thêm hai phần mềm trung gian cấp tuyến đường, sẽ được sử dụng sau trong các tuyến đường của chúng tôi.

isVerifiednotVerified để kiểm tra xem người dùng đã được xác minh hay chưa. Chúng tôi sử dụng các phần mềm trung gian này để chặn quyền truy cập vào các tuyến đường mà chúng tôi muốn chỉ cho phép người dùng đã xác minh truy cập.
Mã:
cd controllerstouch verifyController.js
Trong verifyController.js,
Mã:
const mongoose = require('mongoose')const passport = require('passport')const User = require('../models/user')const { sendVerification, checkVerification } = require('../config/twilioLogic')const loadVerify = async(req, res) => { res.render('verify')}const resendCode = async(req, res) => { sendVerification(req, res, req.user.phonenumber) res.redirect('/users/verify')}const verifyUser = async(req, res) => { //kiểm tra mã xác minh từ đầu vào của người dùng const verifyStatus = await checkVerification(req, res, req.user.phonenumber, req.body.verifyCode) if(verifyStatus === 'approved') { req.session.verified = true res.redirect('/users/dashboard') } else { req.session.verified = false req.flash( 'error_msg', 'mã xác minh sai' ) res.redirect('/users/verify') }}module.exports = { loadVerify, verifyUser, resendCode}
resendCode() gửi lại mã xác minh cho người dùng.

verifyUser sử dụng hàm checkVerification được tạo trong phần trước. Nếu trạng thái là đã phê duyệt, chúng tôi sẽ đặt giá trị đã xác minh trên req.session thành đúng.

req.session chỉ cung cấp một cách hay để truy cập phiên hiện tại. Điều này được thực hiện bởi express-session, thêm đối tượng phiên vào đối tượng yêu cầu của chúng ta.
Do đó, lý do tôi nói rằng hầu hết phần mềm trung gian cấp ứng dụng đều ảnh hưởng đến trạng thái ứng dụng của chúng ta (đối tượng yêu cầu và phản hồi)

Xây dựng các tuyến đường người dùng​

Về cơ bản, ứng dụng của chúng ta sẽ có các tuyến đường sau:
  1. /user/login: để người dùng đăng nhập;
  2. /user/signup: để người dùng đăng ký;
  3. /user/logout: để đăng xuất;
  4. /user/resend: để gửi lại mã xác minh;
  5. /user/verify: để nhập mã xác minh;
  6. /user/dashboard: tuyến đường được bảo vệ bằng Twilio Xác minh.
Mã:
cd routestouch user.js
Trong routes/user.js, nó yêu cầu các gói cần thiết:
Mã:
const express = require('express')const router = express.Router()const { createUser, getSignup } = require('../controllers/signUpController')const { authUser, getLogin } = require('../controllers/loginController')const { loadVerify, verifyUser, resendCode } = require('../controllers/verifyController')const { isLoggedIn, isVerified, notVerified, notLoggedIn } = require('../config/middleware')//tuyến đường đăng nhậprouter.route('/login') .all(notLoggedIn) .get(getLogin) .post(authUser)//tuyến đường đăng nhậprouter.route('/signup') .all(notLoggedIn) .get(getSignup) .post(createUser)//logoutrouter.route('/logout') .get(async (req, res) => { req.logout(); res.redirect('/'); })router.route('/resend') .all(isLoggedIn, notVerified) .get(resendCode)//verify routerouter.route('/verify') .all(isLoggedIn, notVerified) .get(loadVerify) .post(verifyUser)//dashboardrouter.route('/dashboard') .all(isLoggedIn, isVerified) .get(async (req, res) => { res.render('dashboard') })//export routermodule.exports = router
Chúng ta đang tạo các tuyến đường của mình trong đoạn mã trên, hãy cùng xem những gì đang diễn ra ở đây:

router.route() chỉ định tuyến đường. Nếu chúng ta chỉ định router.route('/login'), chúng ta sẽ nhắm mục tiêu đến tuyến đường login. .all([middleware]) cho phép chúng ta chỉ định rằng tất cả các yêu cầu đến tuyến đường đó phải sử dụng các middleware đó.

Cú pháp router.route('/login').all([middleware]).get(getController).post(postController) là một giải pháp thay thế cho cú pháp mà hầu hết các nhà phát triển đã quen sử dụng.

Nó thực hiện cùng một chức năng như router.get('/login', [middleware], getController)router.post('/login, [middleware], postController).
Cú pháp được sử dụng trong mã của chúng ta rất hay vì nó làm cho mã của chúng ta rất DRY — và dễ dàng theo dõi những gì đang diễn ra trong tệp của chúng ta hơn.
Bây giờ, nếu chúng ta chạy ứng dụng của mình bằng cách nhập lệnh bên dưới vào thiết bị đầu cuối của mình:
Mã:
npm run dev
Ứng dụng express đầy đủ của chúng ta sẽ được thiết lập và chạy.

Kết luận​

Những gì chúng ta đã làm trong hướng dẫn này là:
  1. Xây dựng một ứng dụng express;
  2. Thêm hộ chiếu để xác thực bằng các phiên;
  3. Sử dụng Twilio Verify để bảo vệ tuyến đường.
Tôi thực sự hy vọng rằng sau hướng dẫn này, bạn đã sẵn sàng suy nghĩ lại về xác thực dựa trên mật khẩu của mình và thêm lớp bảo mật bổ sung đó vào ứng dụng của mình.

Những gì bạn có thể làm tiếp theo:
  1. Thử khám phá hộ chiếu, sử dụng JWT để xác thực;
  2. Tích hợp những gì bạn đã học ở đây vào một ứng dụng khác;
  3. Khám phá thêm các sản phẩm Twilio. Họ cung cấp các dịch vụ giúp phát triển dễ dàng hơn (Verify chỉ là một trong nhiều dịch vụ).

Đọc thêm về Tạp chí Smashing​

  • “Cách xây dựng ứng dụng trò chuyện nhóm bằng Vanilla JS, Twilio và Node.js,” Zara Cooper
  • “Giữ cho Node.js nhanh: Công cụ, kỹ thuật và mẹo để tạo máy chủ Node.js hiệu suất cao,” David Mark Clements
  • “Cách bảo vệ khóa API của bạn trong quá trình sản xuất bằng tuyến API Next.js,” Caleb Olojo
  • “Cách xây dựng API Node.js cho chuỗi khối Ethereum,” John Agbanusi
 
Back
Bên trên