paint-brush
Hướng dẫn quy trình khôi phục tài khoản bằng NodeJS với Knex và Expresstừ tác giả@antonkalik
678 lượt đọc
678 lượt đọc

Hướng dẫn quy trình khôi phục tài khoản bằng NodeJS với Knex và Express

từ tác giả Anton Kalik12m2024/03/28
Read on Terminal Reader

dài quá đọc không nổi

Hướng dẫn này khám phá việc đặt lại mật khẩu người dùng bằng Nodemailer, Knex và Express. Nó bao gồm việc gửi email để khôi phục mật khẩu và xác thực việc gửi tin nhắn.
featured image - Hướng dẫn quy trình khôi phục tài khoản bằng NodeJS với Knex và Express
Anton Kalik HackerNoon profile picture
0-item

Đây là phân tích chi tiết về cách thiết lập lại cho người dùng khi họ quên mật khẩu cũng như cách gửi email từ Node JS và xác thực việc gửi tin nhắn.

Hầu hết chúng ta đều đã trải qua quá trình khôi phục tài khoản ít nhất một lần — khi quên mật khẩu, cần có các thủ tục để tạo mật khẩu mới và lấy lại quyền truy cập vào hệ thống. Bài viết này tập trung vào việc triển khai quy trình như vậy bằng cách sử dụng Node.js, Knex và một số công cụ chưa được tiết lộ, cùng với Express để xử lý các tuyến đường và thực hiện các hoạt động cần thiết.


Chúng tôi sẽ đề cập đến việc triển khai bộ định tuyến, xử lý các tham số URL, xác định nội dung cần gửi cho người dùng khi chỉ có email hoặc số điện thoại làm bằng chứng, quản lý việc gửi email và giải quyết các mối lo ngại về bảo mật.

Quên mật khẩu

Trước khi đi sâu vào mã hóa, tôi muốn đảm bảo rằng chúng tôi đang làm việc với cùng một cơ sở mã mà bạn có thể truy cập từ công khai của tôi. kho lưu trữ trên GitHub . Chúng tôi sẽ nâng cấp từng bước để triển khai quy trình quên mật khẩu. Để truyền tải email, chúng tôi sẽ sử dụng dịch vụ email của Google.


Bây giờ, hãy xem sơ đồ của quy trình quên mật khẩu.

Lược đồ luồng mật khẩu quên

Máy chủ sẽ chịu trách nhiệm gửi email đến hộp thư người dùng chứa liên kết hợp lệ để đặt lại mật khẩu và cũng sẽ xác thực sự tồn tại của mã thông báo và người dùng.

Gói và di chuyển

Để bắt đầu sử dụng dịch vụ email và gửi email bằng Node.js, chúng tôi cần cài đặt các gói sau ngoài các gói phụ thuộc hiện có:

 npm i --save nodemailer handlebars


Nodemailer : Mô-đun mạnh mẽ cho phép gửi email dễ dàng bằng cách sử dụng SMTP hoặc các cơ chế vận chuyển khác.


Tay cầm : Tay cầm là một công cụ tạo khuôn mẫu phổ biến cho JavaScript. Nó sẽ cho phép chúng ta xác định các mẫu với các phần giữ chỗ có thể chứa đầy dữ liệu khi hiển thị.


Bây giờ, chúng ta cần tạo di chuyển, vì vậy trong trường hợp của tôi, tôi phải thêm một cột mới forgot_password_token vào bảng users :

 knex migrate:make add_field_forgot_password_token -x ts


và trong tệp được tạo, tôi đặt mã:

 import type { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.string('forgot_password_token').unique(); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.dropColumn('forgot_password_token'); }); }

Di chuyển mã thông báo quên mật khẩu trong bảng Người dùng


và sau đó di chuyển tệp mới nhất:

 knex migrate:knex

Vì vậy, bây giờ chúng ta có thể đặt vào bảng users forgot_password_token

Bộ định tuyến

Để quản lý các bộ điều khiển chịu trách nhiệm xử lý logic quên và đặt lại mật khẩu, chúng ta phải thiết lập hai lộ trình. Tuyến đầu tiên bắt đầu quá trình quên mật khẩu, trong khi tuyến thứ hai xử lý quá trình đặt lại, mong đợi tham số mã thông báo trong URL để xác minh. Để thực hiện điều này, hãy tạo một tệp có tên là forgotPasswordRouter.ts trong thư mục src/routes/ và chèn đoạn mã sau:

 import { Router } from 'express'; import { forgotPasswordController } from 'src/controllers/forgotPasswordController'; import { resetPasswordController } from 'src/controllers/resetPasswordController'; export const forgotPasswordRouter = Router(); forgotPasswordRouter.post('/', forgotPasswordController); forgotPasswordRouter.post('/reset/:token', resetPasswordController);

Quên mật khẩu Bộ định tuyến


Hai bộ điều khiển sẽ quản lý logic gửi email và đặt lại mật khẩu.

Quên mật khẩu điều khiển

Khi khách hàng quên mật khẩu, họ không có phiên, điều đó có nghĩa là chúng tôi không thể lấy dữ liệu người dùng ngoại trừ email hoặc bất kỳ thông tin nhận dạng bảo mật nào khác. Trong trường hợp của chúng tôi, chúng tôi đang gửi email để xử lý việc đặt lại mật khẩu. Logic đó chúng ta sẽ đặt vào bộ điều khiển.

 forgotPasswordRouter.post('/', forgotPasswordController);


Ghi nhớ 'quên mật khẩu?' liên kết bên dưới biểu mẫu đăng nhập thường có trong giao diện người dùng của bất kỳ khách hàng nào trong biểu mẫu đăng nhập? Nhấp vào nó sẽ đưa chúng tôi đến chế độ xem nơi chúng tôi có thể yêu cầu đặt lại mật khẩu. Chúng tôi chỉ cần nhập email của mình và bộ điều khiển sẽ xử lý tất cả các thủ tục cần thiết. Hãy kiểm tra đoạn mã sau:


 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; import { TokenService } from 'src/services/TokenService'; import { EmailService } from 'src/services/EmailService'; export const forgotPasswordController = async (req: Request, res: Response) => { try { const { email, }: { email: string; } = req.body; const user = await UserModel.findByEmail(email); if (user) { const token = await TokenService.sign( { id: user.id, }, { expiresIn: '1 day', } ); await user.context.update({ forgot_password_token: token }); await EmailService.sendPasswordResetEmail(email, token); } return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };

Quên mật khẩu điều khiển


Từ phần nội dung, chúng tôi sẽ nhận được một email và sau đó chúng tôi sẽ tìm thấy người dùng bằng cách sử dụng UserModel.findByEmail . Nếu người dùng tồn tại, chúng tôi tạo mã thông báo JWT bằng cách sử dụng TokenService.sign và lưu mã thông báo cho người dùng forgot_password_token khi hết hạn 1 ngày. Sau đó, chúng tôi sẽ gửi tin nhắn đến email kèm theo một liên kết thích hợp cùng với mã thông báo nơi người dùng có thể thay đổi mật khẩu của mình.

Thiết lập Google

Để có thể gửi email, chúng tôi phải tạo địa chỉ email mới sẽ là người gửi.


Hãy truy cập Google để tạo một tài khoản email mới và sau đó, khi tài khoản được tạo, hãy đi tới liên kết Quản lý tài khoản Google của bạn . Bạn có thể tìm thấy nó ở trên cùng bên phải bằng cách nhấp vào hình đại diện. Sau đó, ở menu bên trái, nhấp vào mục Bảo mật , rồi nhấn Xác minh 2 bước . Bên dưới bạn sẽ tìm thấy phần Mật khẩu ứng dụng , nhấp vào mũi tên:


Mật khẩu ứng dụng

Nhập tên cần sử dụng. Trong trường hợp của tôi, tôi đặt Nodemailer và nhấn Create .

Tạo mật khẩu ứng dụng

Sao chép mật khẩu đã tạo và đặt nó vào tệp .env của bạn. Chúng ta cần thiết lập để gửi hai biến:

 MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"


Tất nhiên, để có một email phù hợp như info@company_name.com , bạn phải thiết lập Google Workspace hoặc AWS Amazon WorkMail cùng với AWS SES hoặc bất kỳ dịch vụ nào khác. Nhưng trong trường hợp của chúng tôi, chúng tôi đang sử dụng một tài khoản Gmail đơn giản miễn phí.

Dịch vụ email

Với tệp .env đã được chuẩn bị, chúng tôi đã sẵn sàng thiết lập dịch vụ gửi email của mình. Bộ điều khiển sẽ sử dụng dịch vụ với mã thông báo được tạo và địa chỉ email người nhận cho tin nhắn của chúng tôi.

 await EmailService.sendPasswordResetEmail(email, token);


Hãy tạo src/services/EmailService.ts và xác định lớp cho dịch vụ:

 export class EmailService {}


Và bây giờ là dữ liệu ban đầu, tôi phải lấy môi trường để sử dụng với nodemailer :

 import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; }

Dịch vụ email


Chúng tôi phải quan tâm đến việc khởi tạo dịch vụ. Tôi đã viết về nó trước đây trong bài viết trước của tôi bài báo . Đây là một ví dụ:

 import { TokenService } from 'src/services/TokenService'; import { RedisService } from 'src/services/RedisService'; import { EmailService } from 'src/services/EmailService'; export const initialize = async () => { await RedisService.initialize(); TokenService.initialize(); EmailService.initialize(); };

Khởi tạo dịch vụ


Bây giờ, hãy tiến hành tạo khởi tạo trong lớp EmailService của chúng tôi:

 import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; public static initialize() { try { EmailService.transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.env.USER, pass: this.env.PASS, }, }); } catch (error) { console.error('Error initializing email service'); throw error; } } }

Khởi tạo dịch vụ email


Có khởi tạo nodemailer.createTransport() , một phương thức được cung cấp bởi thư viện nodemailer . Nó tạo ra một đối tượng vận chuyển sẽ được sử dụng để gửi email của chúng tôi. Phương thức này chấp nhận một đối tượng tùy chọn làm đối số trong đó bạn chỉ định chi tiết cấu hình cho bộ vận chuyển.


Chúng tôi đang sử dụng Google: service: 'gmail' chỉ định nhà cung cấp dịch vụ email. Nodemailer cung cấp hỗ trợ tích hợp cho nhiều nhà cung cấp dịch vụ email khác nhau và gmail cho biết rằng trình vận chuyển sẽ được định cấu hình để hoạt động với máy chủ SMTP của Gmail.


Để xác thực auth , cần đặt thông tin xác thực được yêu cầu để truy cập máy chủ SMTP của nhà cung cấp dịch vụ email.


Đối với user nên đặt thành địa chỉ email mà chúng tôi sẽ gửi email và mật khẩu đó đã được tạo trong tài khoản Google từ Mật khẩu ứng dụng.


Bây giờ, hãy thiết lập phần cuối cùng của dịch vụ của chúng tôi:

 import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { generateAttachments } from 'src/helpers/generateAttachments'; import { generateTemplate } from 'src/helpers/generateTemplate'; import { getHost } from 'src/helpers/getHost'; dotenv.config(); export class EmailService { // ...rest code public static async sendPasswordResetEmail(email: string, token: string) { try { const host = getHost(); const template = generateTemplate<{ token: string; host: string; }>('passwordResetTemplate', { token, host }); const attachments = generateAttachments([{ name: 'email_logo' }]); const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); console.log('Message sent: %s', info.messageId); } catch (error) { console.error('Error sending email: ', error); } } }

Gửi email đặt lại mật khẩu


Trước khi tiếp tục, điều quan trọng là phải xác định máy chủ lưu trữ thích hợp khi khách hàng nhận được email. Việc thiết lập liên kết bằng mã thông báo trong nội dung email là điều cần thiết.


 import * as dotenv from 'dotenv'; import process from 'process'; dotenv.config(); export const getHost = (): string => { const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; const port = isProduction ? '' : `:${process.env.CLIENT_PORT}`; return `${protocol}://${process.env.WEB_HOST}${port}`; };

Nhận máy chủ


Đối với các mẫu, tôi đang sử dụng handlebars và để làm được điều đó, chúng ta cần tạo trong src/temlates/passwordResetTemplate.hbs mẫu HTML đầu tiên của mình:


 <!-- passwordResetTemplate.hbs --> <html lang='en'> <head> <style> a { color: #372aff; } .token { font-weight: bold; } </style> <title>Forgot Password</title> </head> <body> <p>You requested a password reset. Please use the following link to reset your password:</p> <a class='token' href="{{ host }}/reset-password/{{ token }}">Reset Password</a> <p>If you did not request a password reset, please ignore this email.</p> <img src="cid:email_logo" alt="Email Logo"/> </body> </html>

Mẫu đặt lại mật khẩu


và bây giờ chúng ta có thể sử dụng lại mẫu này với trình trợ giúp:

 import path from 'path'; import fs from 'fs'; import handlebars from 'handlebars'; export const generateTemplate = <T>(name: string, props: T): string => { const templatePath = path.join(__dirname, '..', 'src/templates', `${name}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); return template(props); };

Tạo trình trợ giúp mẫu


Để nâng cao chất lượng email của chúng tôi, chúng tôi thậm chí có thể bao gồm các tệp đính kèm. Để làm như vậy, hãy thêm tệp email_logo.png vào thư mục src/assets . Sau đó, chúng tôi có thể hiển thị hình ảnh này trong email bằng chức năng trợ giúp sau:


 import path from 'path'; import { Extension } from 'src/@types/enums'; type AttachmentFile = { name: string; ext?: Extension; cid?: string; }; export const generateAttachments = (files: AttachmentFile[] = []) => files.map(file => { const ext = file.ext || Extension.png; const filename = `${file.name}.${ext}`; const imagePath = path.join(__dirname, '..', 'src/assets', filename); return { filename, path: imagePath, cid: file.cid || file.name, }; });

Trình trợ giúp tạo tệp đính kèm


Sau khi thu thập tất cả những người trợ giúp đó, chúng tôi phải có thể gửi email bằng cách sử dụng:

 const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, });


Cách tiếp cận này mang lại khả năng mở rộng tốt, cho phép dịch vụ sử dụng nhiều phương pháp khác nhau để gửi email có nội dung đa dạng.


Bây giờ, hãy thử kích hoạt bộ điều khiển bằng bộ định tuyến của chúng tôi và gửi email. Đối với điều đó, tôi đang sử dụng Người phát thơ :

Gửi email từ người đưa thư

Bảng điều khiển sẽ cho bạn biết rằng tin nhắn đã được gửi:

 Message sent: <1k96ah55-c09t-p9k2–[email protected]>


Kiểm tra tin nhắn mới trong hộp thư đến:

Hộp thư đến có email mới từ máy chủ

Liên kết tới Đặt lại mật khẩu phải chứa mã thông báo và máy chủ:

 http://localhost:3000/reset-password/<token>


Cổng 3000 được chỉ định ở đây vì thông báo này liên quan đến quá trình phát triển. Điều này cho thấy rằng ứng dụng khách chịu trách nhiệm xử lý các biểu mẫu đặt lại mật khẩu cũng sẽ hoạt động trong môi trường phát triển.

Đặt lại mật khẩu

Mã thông báo phải được xác thực ở phía bộ điều khiển bằng TokenService từ đó chúng tôi có thể nhận được người dùng đã gửi email đó. Hãy khôi phục bộ định tuyến sử dụng mã thông báo:

 forgotPasswordRouter.post('/reset/:token', resetPasswordController);


Bộ điều khiển sẽ chỉ cập nhật mật khẩu nếu mã thông báo hợp lệ và chưa hết hạn, theo thời gian hết hạn được đặt thành một giờ. Để triển khai chức năng này, hãy điều hướng đến thư mục src/controllers/ và tạo một tệp có tên resetPasswordController.ts chứa mã sau:

 import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { TokenService } from 'src/services/TokenService'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const resetPasswordController = async (req: Request, res: Response) => { try { const token = req.params.token; if (!token) { return res.sendStatus(400); } const userData = await TokenService.verify<{ id: number }>(token); const user = await UserModel.findOneById<User>(userData.id); if (!user) { return res.sendStatus(400); } const newPassword = req.body.password; if (!newPassword) { return res.sendStatus(400); } const hashedPassword = await bcrypt.hash(newPassword, 10); await UserModel.updateById(user.id, { password: hashedPassword, passwordResetToken: null }); return res.sendStatus(200); } catch (error) { const errors = ['jwt malformed', 'TokenExpiredError', 'invalid token']; if (errors.includes(error.message)) { return res.sendStatus(400); } return res.sendStatus(500); } };

Đặt lại bộ điều khiển mật khẩu


Bộ điều khiển này sẽ nhận mã thông báo, xác minh nó, trích xuất ID người dùng từ dữ liệu được giải mã, truy xuất người dùng tương ứng, lấy mật khẩu mới do khách hàng gửi trong nội dung yêu cầu và tiến hành cập nhật mật khẩu trong cơ sở dữ liệu. Cuối cùng, điều này cho phép khách hàng đăng nhập bằng mật khẩu mới.

Phần kết luận

Khả năng mở rộng của dịch vụ email được thể hiện thông qua nhiều cách tiếp cận khác nhau, chẳng hạn như gửi xác nhận hoặc thông báo thành công, như những cách chỉ báo cập nhật mật khẩu và cho phép đăng nhập tiếp theo. Tuy nhiên, việc quản lý mật khẩu là một thách thức lớn, đặc biệt khi việc tăng cường bảo mật ứng dụng là điều cấp thiết.


Có nhiều tùy chọn có sẵn để tăng cường bảo mật, bao gồm kiểm tra bổ sung trước khi cho phép thay đổi mật khẩu, chẳng hạn như so sánh mã thông báo, email và xác thực mật khẩu.


Một tùy chọn khác là triển khai hệ thống mã PIN, trong đó mã được gửi đến email của người dùng để xác thực ở phía máy chủ. Mỗi biện pháp này đều yêu cầu sử dụng khả năng gửi email.


Tất cả mã được triển khai bạn có thể tìm thấy trong Kho lưu trữ GitHub tại đây .


Vui lòng tiến hành bất kỳ thử nghiệm nào với bản dựng này và chia sẻ phản hồi của bạn về những khía cạnh mà bạn đánh giá cao về chủ đề này. Cảm ơn bạn rất nhiều.

Người giới thiệu

Tại đây, bạn có thể tìm thấy một số tài liệu tham khảo mà tôi đã sử dụng trong bài viết này:


Cũng được xuất bản ở đây