Tạo giải pháp xác thực mạnh mẽ cho máy chủ Node js bằng cách sử dụng Knex để quản lý cơ sở dữ liệu, Redis để lưu vào bộ nhớ đệm hiệu quả và Express để định tuyến liền mạch. Trong quá trình tìm kiếm giải pháp xác thực nhanh chóng, trực quan và hợp lý cho các ứng dụng Node.js của mình, tôi đã gặp phải các tình huống yêu cầu triển khai nhanh chóng mà không ảnh hưởng đến chức năng. Từ đăng ký và đăng nhập của người dùng đến quản lý mật khẩu bị quên, cập nhật dữ liệu người dùng và thậm chí xóa tài khoản, tôi đã tìm kiếm một giải pháp toàn diện giúp điều hướng liền mạch thông qua các tương tác thiết yếu này của người dùng. Vì vậy, bài viết của tôi nhằm mục đích trình bày chính xác điều đó - một cách tiếp cận gắn kết tích hợp các phương pháp rõ ràng để triển khai xác thực và bộ nhớ đệm, đảm bảo luồng người dùng mạnh mẽ và hiệu quả. Ở đây, chúng ta sẽ bỏ qua các quy trình cài đặt cơ bản và tạo mô hình, trực tiếp tập trung vào sự phức tạp của xác thực và luồng người dùng. Chúng tôi sẽ bao gồm tất cả các liên kết cần thiết để lấy tệp cấu hình trong suốt bài viết, đảm bảo quyền truy cập liền mạch vào các tài nguyên cần thiết để thiết lập. Công cụ Để triển khai việc này, chúng tôi sẽ tận dụng Node.js phiên bản 20.11.1 cùng với Knex, Express và Redis. Ngoài ra, chúng tôi sẽ sử dụng PostgreSQL làm cơ sở dữ liệu, cơ sở dữ liệu này sẽ được đóng gói và điều phối bằng Docker để quản lý liền mạch. Tên ứng dụng của chúng ta sẽ là . Hãy tạo thư mục đó và chạy bên trong để tạo cơ bản user-flow-boilerplate npm init -y package.json { "name": "user-flow-boilerplate", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } Gói ban đầu.json Bước tiếp theo là thêm các phụ thuộc cần thiết: : phụ thuộc npm i -S bcrypt body-parser cors dotenv express jsonwebtoken knex pg redis validator : devDependency npm i -D @babel/core @babel/eslint-parser @babel/plugin-transform-class-properties @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript @faker-js/faker @types/bcrypt @types/body-parser @types/cors @types/express @types/jest @types/jsonwebtoken @types/node @types/node-cron @types/validator @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-jest cross-env eslint eslint-config-prettier eslint-plugin-prettier jest nodemon npm-run-all prettier ts-jest ts-loader ts-node tsconfig-paths tslint typescript webpack webpack-cli webpack-node-externals và thêm các tập lệnh sẽ xây dựng và chạy ứng dụng của chúng tôi: "scripts": { "start": "NODE_ENV=production node dist/bundle.js", "build": "NODE_ENV=production webpack --config webpack.config.js", "dev": "cross-env NODE_ENV=development && npm-run-all -p dev:*", "dev:build": "webpack --config webpack.config.js --watch", "dev:start": "nodemon --watch dist --exec node dist/bundle.js", "test": "NODE_ENV=test jest --config ./jest.config.js", "lint": "eslint ./src -c .eslintrc.json" }, Để đảm bảo ứng dụng của chúng ta khởi chạy suôn sẻ, điều cần thiết là phải tạo một thư mục và đặt tệp điểm nhập ban đầu của chúng ta, , trong đó. src index.ts require('dotenv').config(); import process from 'process'; import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; const app = express(); const PORT = process.env.PORT || 9999; app.use(bodyParser.json()); app.use(cors()); app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); (async () => { try { app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })(); Tệp điểm vào Để phát triển, chúng ta cần có các cài đặt cho , , , , , . Tất cả những tệp đó tôi đã mô tả trong bài viết sau: . typscript lint jest bable prettier nodemon Tạo máy chủ Node.js bằng Postgres và Knex trên Express Sau khi định cấu hình tất cả cài đặt và tạo điểm vào, việc thực thi sẽ khởi động máy chủ và bạn sẽ thấy đầu ra tương tự như sau: npm run dev ./src/index.ts 1.7 KiB [built] [code generated] external "dotenv" 42 bytes [built] [code generated] external "process" 42 bytes [built] [code generated] external "express" 42 bytes [built] [code generated] external "body-parser" 42 bytes [built] [code generated] external "cors" 42 bytes [built] [code generated] webpack 5.90.3 compiled successfully in 751 ms [nodemon] restarting due to changes... [nodemon] starting `node dist/bundle.js` Server is running on port 9999 Tiếp theo, điều hướng đến nơi chúng tôi sẽ thiết lập một bộ sưu tập dành riêng cho việc kiểm tra các điểm cuối của chúng tôi. Trong bộ sưu tập mới, thêm yêu cầu mới, nhấn (trên máy Mac, nhưng các phím phụ thuộc vào hệ điều hành của bạn) và đặt tên là . Người phát thơ GET cmd + E health Thêm nhập cho URL: . Đối với , hãy thêm một biến mới mà bạn sẽ sử dụng trên toàn bộ bộ sưu tập: {{BASE_URI}}/health BASE_URI http://localhost:9999/api/v1 Sau đó, chỉ cần nhấp vào nút 'Gửi' và bạn sẽ quan sát nội dung phản hồi: { "message": "OK" } Cơ sở dữ liệu Trước khi tiếp tục, điều quan trọng là phải thiết lập và chạy cơ sở dữ liệu của chúng tôi. Chúng tôi sẽ thực hiện điều này bằng cách khởi chạy nó với . Để truy cập và quản lý cơ sở dữ liệu, bạn có thể sử dụng nhiều nền tảng phát triển khác nhau như . docker-compose pgAdmin Cá nhân tôi thích sử dụng hơn , được trang bị trình điều khiển cho phép kết nối liền mạch với cơ sở dữ liệu PostgreSQL để quản lý hiệu quả. RubyMỏ Chúng tôi cần tệp với các khóa, mật khẩu và tên kiểm tra cần thiết: .env PORT=9999 WEB_HOST="localhost" # DB DB_HOST="localhost" DB_PORT=5432 DB_NAME="user_flow_boilerplate" DB_USER="username_123" DB_PASSWORD="SomeParole999" # User DEFAULT_PASSWORD="SomeParole999" JWT_SECRET="6f1d7e9b9ba56476ae2f4bdebf667d88eeee6e6c98c68f392ed39f7cf6e51c5a" # Test User TEST_EMAIL="test_email@test.com" TEST_USERNAME="test_username" TEST_PASSWORD="SomeParole999" # Redis REDIS_HOST="localhost" REDIS_PORT=6379 REDIS_DB=0 REDIS_PASSWORD="SomeParole999" .env để kết nối tới cơ sở dữ liệu, Redis và kiểm tra các giá trị cho hạt giống Đừng lo, tôi đã tạo ngẫu nhiên để minh họa nó một cách chân thực hơn. Vì vậy, hãy tạo một tệp ở thư mục gốc của dự án: JWT_SECRET docker-compose.yml version: '3.6' volumes: data: services: database: build: context: . dockerfile: postgres.dockerfile image: postgres:latest container_name: postgres environment: TZ: Europe/Madrid POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} networks: - default volumes: - data:/var/lib/postgresql/data ports: - "5432:5432" restart: unless-stopped redis: image: redis:latest container_name: redis command: redis-server --requirepass ${REDIS_PASSWORD} networks: - default ports: - "6379:6379" restart: unless-stopped tập tin soạn thảo docker với các dịch vụ Chúng tôi sẽ triển khai hai dịch vụ trong Docker để kết nối nhanh chóng. Tôi đã hợp lý hóa quy trình này để tạo điều kiện truy cập nhanh vào cơ sở dữ liệu hoặc Redis, cho phép chúng tôi truy xuất dữ liệu một cách hiệu quả. Vì vậy, hãy chạy các dịch vụ đó và chúng ta phải có thể thấy đầu ra sau đầu ra sau: docker-compose up docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e4bef95de1dd postgres:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp postgres 365e3a68351a redis:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:6379->6379/tcp redis Bây giờ, chúng ta cần tạo tệp nơi chúng ta lưu trữ các loại của mình cho ứng dụng: src/@types/index.ts export enum Role { Admin = 'admin', Blogger = 'blogger', } export type UserSession = { id: number; }; export type DatabaseDate = { created_at: Date; updated_at: Date; }; export type DefaultUserData = { role: Role; }; export interface User extends DatabaseDate { id: number; email: string; username: string; password: string; role: Role; } Các loại dịch vụ Tại thời điểm này, bạn cần có trong thư mục gốc của dự án và cơ sở dữ liệu để kết nối, di chuyển và hạt giống. knexfile.ts Tôi đã để lại lời giải thích khá chi tiết trong bài viết về cách di chuyển và đưa người dùng vào cơ sở dữ liệu nơi chúng tôi đang sử dụng các biến env đó. Tạo máy chủ Node.js bằng Postgres và Knex trên Express Tôi muốn kiểm tra cụ thể việc di chuyển để đảm bảo rằng chúng tôi ở trên cùng một trang. Chúng tôi đã triển khai dịch vụ của mình và chúng tôi phải có khả năng kiểm tra kết nối với cơ sở dữ liệu. docker exec -it postgres psql -U username_123 user_flow_boilerplate Nếu kết nối tốt thì bạn sẽ ở trong bảng điều khiển . Được rồi, nếu kết nối không có vấn đề gì thì chúng ta có thể di chuyển các bảng của mình đến đó. Chạy . Sau đó, bạn nên quan sát các cột mới được thêm vào trong bảng trong cơ sở dữ liệu. psql knex migrate:latest users Hãy gieo nó với dữ liệu giả và kiểm tra lại bảng. knex seed:run Vì vậy, giờ đây chúng ta đã được trang bị để thao tác với cơ sở dữ liệu, cho phép chúng ta thêm, xóa hoặc cập nhật người dùng khi cần. Bộ định tuyến Cuối cùng, chúng ta có thể quên đi cài đặt và sự chuẩn bị mà tập trung cụ thể vào luồng người dùng. Để làm được điều đó, chúng ta cần tạo một bộ định tuyến. Chúng ta cần xử lý bằng bộ định tuyến đó các hoạt động sau: , , , , . login logout signup delete_user update_user Để làm điều đó, trên , hãy thêm đoạn mã sau: src/routes/index.ts import { Router } from 'express'; import { authRouter } from 'src/routes/authRouter'; import { healthController } from 'src/controllers/healthController'; import { sessionController } from 'src/controllers/sessionController'; import { authMiddleware } from 'src/middlewares/authMiddleware'; import { userRouter } from 'src/routes/userRouter'; export const router = Router({ mergeParams: true }); router.get('/health', healthController); router.use('/auth', authRouter); router.get('/session', authMiddleware, sessionController); router.use('/user', authMiddleware, userRouter); router.use((_, res) => { return res.status(404).json({ message: 'Not Found' }); }); Tệp tuyến đường Như bạn có thể thấy, lúc đầu, chúng tôi đã thêm tuyến đường mà chúng tôi đã kiểm tra. Vì vậy, hãy cập nhật điểm vào để áp dụng các tuyến đường đó ở đó. Đầu tiên, xóa trước đó. /health get -> REMOVE -> app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); và thêm vào đầu tập tin: import { router } from 'src/routes'; // ... app.use(cors()); app.use('/api/v1', router); và tạo bộ điều khiển đầu tiên để kiểm tra bằng mã: health src/controllers/healthController.ts import { Request, Response } from 'express'; export const healthController = (_: Request, res: Response) => res.status(200).send('ok'); Kiểm soát sức khỏe Bây giờ, hãy quay lại bộ định tuyến và kiểm tra xem chúng ta có gì để thêm vào các tuyến đường. Chúng ta cần thêm hai tệp nữa: và authRouter.ts userRouter.ts import { Router } from 'express'; import { signUpController } from 'src/controllers/auth/signUpController'; import { loginController } from 'src/controllers/auth/loginController'; export const authRouter = Router(); authRouter.post('/signup', signUpController); authRouter.post('/login', loginController); Bộ định tuyến xác thực import { Router } from 'express'; import { updateUserController } from 'src/controllers/user/updateUserController'; import { deleteUserController } from 'src/controllers/user/deleteUserController'; import { logoutController } from 'src/controllers/user/logoutController'; import { updatePasswordController } from 'src/controllers/user/updatePasswordController'; export const userRouter = Router(); userRouter.patch('/', updateUserController); userRouter.delete('/', deleteUserController); userRouter.post('/logout', logoutController); userRouter.post('/update-password', updatePasswordController); Bộ định tuyến người dùng Tôi đã chia logic này để dễ đọc và có trách nhiệm duy trì chức năng riêng biệt. Tất cả các tuyến đường đó đều cần bộ điều khiển để chúng tôi xử lý logic. Các tuyến xác thực và tình trạng không cần phần mềm trung gian xác thực, vì vậy các tuyến đó không được bảo vệ, nhưng nếu không có kết quả trùng khớp, chúng tôi sẽ nhận được trạng thái 404. router.get('/health', healthController); router.use('/auth', authRouter); Bây giờ, khi chúng ta đã giải quyết xong tất cả các tuyến đường, chúng ta phải thiết lập mô hình người dùng. Mô hình người dùng Tôi sẽ sử dụng mô hình cơ sở cho mô hình người dùng, từ đó tôi sẽ sử dụng lại các phương thức CRUD. Mặc dù trước đây tôi đã trình bày việc tạo mô hình trong một bài khác , Tôi sẽ đưa mô hình cơ sở vào đây để dễ hiểu và dễ hiểu hơn. Tạo trong bài báo src/models/Model.ts import { database } from 'root/database'; export abstract class Model { protected static tableName?: string; protected static get table() { if (!this.tableName) { throw new Error('The table name must be defined for the model.'); } return database(this.tableName); } public static async insert<Payload>(data: Payload): Promise<{ id: number; }> { const [result] = await this.table.insert(data).returning('id'); return result; } public static async updateOneById<Payload>( id: number, data: Payload ): Promise<{ id: number; }> { const [result] = await this.table.where({ id }).update(data).returning('id'); return result; } public static async delete(id: number): Promise<number> { return this.table.where({ id }).del(); } public static async findOneById<Result>(id: number): Promise<Result> { return this.table.where('id', id).first(); } public static async findOneBy<Payload, Result>(data: Payload): Promise<Result> { return this.table.where(data as string).first(); } } Mô hình cơ sở Với mô hình cơ sở, chúng ta phải có khả năng tạo trong cùng một thư mục: UserModel.ts import { Model } from 'src/models/Model'; import { Role, User, DefaultUserData } from 'src/@types'; export class UserModel extends Model { static tableName = 'users'; public static async create<Payload>(data: Payload) { return super.insert<Payload & DefaultUserData>({ ...data, role: data.role || Role.Blogger, }); } public static findByEmail(email: string): Promise<User | null> { return this.findOneBy< { email: string; }, User >({ email }); } public static findByUsername(username: string): Promise<User | null> { return this.findOneBy< { username: string; }, User >({ username }); } } Mô hình người dùng Trong mô hình người dùng, tôi chỉ đặt theo mặc định nếu không được cung cấp từ tải trọng. Và bây giờ chúng ta đã có sẵn các mô hình của mình, chúng ta có thể tiến hành sử dụng chúng trong bộ điều khiển và phần mềm trung gian của mình. role Phần mềm trung gian xác thực Phần mềm trung gian xác thực trong ứng dụng Node.js chịu trách nhiệm xác thực các yêu cầu đến, đảm bảo rằng chúng đến từ người dùng hợp lệ và được ủy quyền. Nó thường chặn các yêu cầu đến, trích xuất mã thông báo hoặc thông tin xác thực và xác minh tính hợp lệ của chúng dựa trên cơ chế xác thực được xác định trước, chẳng hạn như JWT (Mã thông báo web JSON) trong trường hợp này. Nếu quá trình xác thực thành công, phần mềm trung gian sẽ cho phép yêu cầu chuyển sang trình xử lý tiếp theo trong chu trình phản hồi yêu cầu. Tuy nhiên, nếu xác thực không thành công, nó sẽ phản hồi bằng mã trạng thái HTTP thích hợp (ví dụ: 401 trái phép) và tùy chọn cung cấp thông báo lỗi. Tạo thư mục và thêm vào đó một tệp với mã sau: src/middlewares authMiddleware.ts import { jwt } from 'src/utils/jwt'; import { Redis } from 'src/redis'; import type { Request, Response, NextFunction } from 'express'; import type { UserSession } from 'src/@types'; export async function authMiddleware(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; const JWT_SECRET = process.env.JWT_SECRET; if (!token) return res.sendStatus(401); if (!JWT_SECRET) { console.error('JWT_SECRET Not Found'); return res.sendStatus(500); } if (!token) return res.status(401).json({ error: 'Token not provided' }); try { const userSession = await jwt.verify<UserSession>(token); if (!userSession) { return res.sendStatus(401); } const storedToken = await Redis.getSession(userSession.id); if (!storedToken || storedToken !== token) { return res.sendStatus(401); } req.user = userSession; next(); } catch (error) { console.error('JWT_ERROR', error); return res.sendStatus(401); } } Xác thực tệp phần mềm trung gian Phần mềm trung gian xác thực trích xuất mã thông báo JWT từ tiêu đề yêu cầu, xác minh tính hợp lệ của nó bằng thư viện JWT và kiểm tra xem mã thông báo có khớp với mã được lưu trữ trong Redis hay không. Nếu mã thông báo hợp lệ và khớp với mã thông báo được lưu trữ, phần mềm trung gian sẽ đặt phiên người dùng được xác thực trên đối tượng yêu cầu ( ) và gọi hàm để chuyển quyền điều khiển cho phần mềm trung gian hoặc trình xử lý tuyến tiếp theo. Nếu không, nó sẽ phản hồi bằng mã trạng thái 401 cho biết xác thực không thành công. req.user next() Mã thông báo Web JSON Hãy cùng xem lại công dụng của jwt. Tạo trong tệp với mã sau: src/utils/jwt.ts require('dotenv').config(); import jsonwebtoken from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET as string; export const jwt = { verify: <Result>(token: string): Promise<Result> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { jsonwebtoken.verify(token, JWT_SECRET, (error, decoded) => { if (error) { reject(error); } else { resolve(decoded as Result); } }); }); }, sign: (payload: string | object | Buffer): Promise<string> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { try { resolve(jsonwebtoken.sign(payload, JWT_SECRET)); } catch (error) { reject(error); } }); }, }; Tệp tiện ích JWT Tiện ích này đóng vai trò quan trọng trong việc xử lý Mã thông báo Web JSON trong ứng dụng Node.js. Đối tượng xuất các chức năng cho cả việc ký và xác minh JWT, tận dụng thư viện . Các chức năng này tạo điều kiện thuận lợi cho việc tạo và xác thực JWT, cần thiết để triển khai các cơ chế xác thực trong ứng dụng. jwt jsonwebtoken Tiện ích gói gọn chức năng xử lý JWT, đảm bảo cơ chế xác thực an toàn trong ứng dụng Node.js đồng thời tuân thủ các phương pháp hay nhất để quản lý biến môi trường. Làm lại Được sử dụng làm cơ sở dữ liệu, bộ nhớ đệm và môi giới tin nhắn. Thường được sử dụng trong nhiều trường hợp sử dụng, bao gồm bộ nhớ đệm, quản lý phiên, phân tích thời gian thực, hàng đợi nhắn tin, bảng xếp hạng, v.v. Việc kiểm tra mã thông báo từ Redis đóng vai trò như một lớp bảo mật và xác thực bổ sung cho mã thông báo JWT. Hãy đi sâu vào cài đặt. Để làm điều đó, hãy tạo trong tệp với đoạn mã sau: src/redis/index.ts require('dotenv').config({ path: '../../.env', }); import process from 'process'; import * as redis from 'redis'; const client = redis.createClient({ url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, }); client.on('error', error => console.error('Redis Client Error', error)); const connect = async () => { try { await client.connect(); console.log('Connected to Redis'); } catch (err) { console.error(`Could not connect to Redis: ${err}`); process.exit(1); } }; class Redis { public static setSession(userId: number, token: string) { if (!userId) throw new Error('userId is required'); if (!token) throw new Error('token is required'); try { return client.set(`session:${userId}`, token); } catch (error) { console.error(error); } } public static getSession(userId: number) { if (!userId) throw new Error('userId is required'); return client.get(`session:${userId}`); } public static deleteSession(userId: string) { if (!userId) throw new Error('userId is required'); try { return client.del(`session:${userId}`); } catch (error) { console.error(error); } } } export { client, connect, Redis }; Cửa hàng phiên Redis Bằng Redis, chúng tôi sẽ lưu trữ và quản lý mã thông báo phiên của người dùng. Trong phần mềm trung gian xác thực, sau khi xác minh tính xác thực của mã thông báo JWT, phần mềm trung gian sẽ kiểm tra xem mã thông báo có tồn tại hay không và khớp với mã được lưu trữ trong Redis cho phiên người dùng tương ứng. Điều này giúp đảm bảo rằng chỉ những người dùng hợp lệ và được ủy quyền mới có thể truy cập các tuyến đường được bảo vệ. Redis được sử dụng làm kho lưu trữ khóa-giá trị để duy trì mã thông báo phiên của người dùng. Khi người dùng đăng nhập hoặc xác thực, mã thông báo phiên của họ sẽ được lưu trữ trong Redis. Điều này cho phép truy xuất mã thông báo phiên một cách hiệu quả và nhanh chóng trong các lần kiểm tra xác thực tiếp theo. Redis được sử dụng trong phần mềm trung gian xác thực để quản lý phiên hiệu quả, trong khi tệp liên quan đến Redis xử lý cấu hình và kết nối với máy chủ Redis, đồng thời cung cấp các chức năng tương tác với Redis trong các phần khác của ứng dụng. Thiết lập này đảm bảo cơ chế xác thực an toàn và đáng tin cậy, với mã thông báo phiên người dùng được lưu trữ và quản lý trong Redis. Phần cuối cùng là chúng ta phải kết nối với Redis tại điểm vào: // all imports import * as Redis from 'src/redis'; const app = express(); const PORT = process.env.PORT || 9999; // middlewares (async () => { try { await Redis.connect(); app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })(); Kết nối với Redis Sau khi hoàn tất việc chuẩn bị xác thực, bây giờ chúng ta có thể chuyển trọng tâm sang bộ điều khiển. Bộ điều khiển Bộ điều khiển trong các tuyến giúp tổ chức logic của ứng dụng bằng cách tách biệt các mối quan tâm và thúc đẩy khả năng bảo trì mã. Chúng tôi đã tạo bộ điều khiển để kiểm tra tình trạng. Tiếp theo, chúng ta sẽ tiến hành tạo bộ điều khiển để xử lý các thao tác với người dùng. Bộ điều khiển đầu tiên mà chúng ta sẽ sử dụng là phải nằm trong với mã sau: sessionController.ts src/controllers import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const user = await UserModel.findOneById<User>(req.user.id); if (user) { return res.status(200).json(user); } else { return res.sendStatus(401); } } catch (error) { return res.sendStatus(500); } }; Bộ điều khiển phiên Bộ điều khiển này phục vụ mục đích xử lý điểm cuối liên quan đến phiên, có khả năng chịu trách nhiệm truy xuất thông tin về người dùng hiện được xác thực. Chúng tôi cần bộ điều khiển này vì những lý do sau: Bộ điều khiển này cho phép ứng dụng truy xuất thông tin về phiên của người dùng, chẳng hạn như hồ sơ người dùng của họ hoặc dữ liệu liên quan khác. Thông tin này có thể hữu ích cho việc tùy chỉnh trải nghiệm người dùng hoặc cung cấp nội dung được cá nhân hóa dựa trên hồ sơ của người dùng. Thông tin phiên người dùng: Bằng cách kiểm tra xem có tồn tại hay không, bộ điều khiển đảm bảo rằng chỉ những người dùng được xác thực mới có thể truy cập điểm cuối. Điều này giúp thực thi các quy tắc xác thực và ủy quyền, đảm bảo rằng chỉ những người dùng được ủy quyền mới có thể truy cập được dữ liệu nhạy cảm của người dùng. Xác thực và ủy quyền: req.user Bộ điều khiển truy vấn cơ sở dữ liệu (sử dụng ) để truy xuất thông tin của người dùng dựa trên ID phiên của họ. Điều này cho phép ứng dụng tìm nạp dữ liệu cụ thể của người dùng một cách linh hoạt, cung cấp trải nghiệm phù hợp cho từng người dùng. Phần này chắc chắn có thể được cải thiện bằng bộ đệm Redis: Truy xuất hồ sơ người dùng: UserModel import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const cachedProfile = await Redis.getSession(req.user.id); if (cachedProfile) { return res.status(200).json(JSON.parse(cachedProfile)); } else { const user = await UserModel.findOneById<User>(req.user.id); if (user) { await Redis.setSession(req.user.id, JSON.stringify(user), CACHE_EXPIRATION); return res.status(200).json(user); } else { return res.sendStatus(401); } } } catch (error) { console.error('Error retrieving user profile:', error); return res.sendStatus(500); } }; Tệp Trình điều khiển phiên với phiên được đặt Redis Chúng tôi xác định hằng số để chỉ định thời gian hết hạn bộ đệm tính bằng giây. Trong ví dụ này, nó được đặt thành 3600 giây (1 giờ). Dữ liệu được lưu trong bộ nhớ đệm được làm mới định kỳ, ngăn không cho dữ liệu cũ được cung cấp cho người dùng và duy trì tính toàn vẹn của dữ liệu trong bộ nhớ đệm. CACHE_EXPIRATION Trước khi tiếp tục tạo , quản lý quá trình đăng ký cho người dùng mới trong ứng dụng của chúng ta, hãy xem lại lược đồ: signUpController Trong trường hợp của chúng tôi, khi cố gắng đăng ký bằng email hiện có trong cơ sở dữ liệu, chúng tôi ưu tiên quyền riêng tư của người dùng bằng cách không tiết lộ rõ ràng liệu người dùng đó có tồn tại hay không. Thay vào đó, chúng tôi thông báo cho khách hàng bằng một thông báo chung nêu rõ . Invalid email or password Cách tiếp cận này khuyến khích khách hàng gửi thông tin xác thực hợp lệ mà không tiết lộ thông tin không cần thiết về người dùng hiện tại. Bây giờ hãy tạo và thêm mã sau: src/controllers/auth/signUpController.ts import bcrypt from 'bcrypt'; import { jwt } from 'src/utils/jwt'; import { Request, Response } from 'express'; import { validate } from 'src/helpers/validation/validate'; import { userSchema } from 'src/helpers/validation/schemas/userSchema'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; import { getRandomString } from 'src/utils/getRandomString'; type Payload = Omit<User, 'id' | 'created_at' | 'updated_at' | 'role'>; export async function signUpController(req: Request, res: Response) { const { email, password }: Payload = req.body; const validation = validate<Payload>(req.body, userSchema); if (!validation.isValid) { return res.status(400).send(`Invalid ${validation.invalidKey}`); } try { const user = await UserModel.findOneBy({ email }); if (user) { return res.status(400).json({ message: 'Invalid email or password' }); } const hashedPassword = (await bcrypt.hash(password, 10)) as string; const username = `${email.split('@')[0]}${getRandomString(5)}`; const createdUser = await UserModel.create<Payload>({ email, password: hashedPassword, username, }); const token = await jwt.sign({ id: createdUser.id, }); await Redis.setSession(createdUser.id, token); res.status(200).json({ token, }); } catch (error) { return res.sendStatus(500); } } Đăng ký điều khiển Bộ điều khiển nhận được yêu cầu chứa email và mật khẩu của người dùng, thường là từ biểu mẫu đăng ký. Nó xác thực dữ liệu đến dựa trên được xác định trước để đảm bảo dữ liệu đáp ứng định dạng được yêu cầu. userSchema Nếu quá trình xác thực thành công, cho biết không có người dùng hiện tại và các trường hợp lệ, bộ điều khiển sẽ tiến hành băm mật khẩu bằng cách sử dụng , tạo và tạo người dùng bằng cách sử dụng . bcrypt.hash username UserModel.create Cuối cùng, nó tạo bằng cách sử dụng , đặt dữ liệu trong và gửi lại cho người dùng. token jwt session Redis token Bây giờ, hãy tập trung vào việc tạo bộ điều khiển đăng nhập. Tạo tệp : src/controllers/auth/loginController.ts require('dotenv').config({ path: '../../.env', }); import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { jwt } from 'src/utils/jwt'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export async function loginController(req: Request, res: Response) { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: 'Invalid email or password' }); } try { const user = await UserModel.findByEmail(email); if (user) { const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(400).json({ message: 'Invalid email or password' }); } const token: string = await jwt.sign({ id: user.id, }); await Redis.setSession(user.id, token); res.status(200).json({ token }); } else { return res.status(400).json({ message: 'Invalid email or password' }); } } catch (error) { console.error(error); return res.sendStatus(500); } } Bộ điều khiển đăng nhập Về cơ bản, chúng tôi bắt đầu bằng cách xác thực các trường được cung cấp và sau đó kiểm tra sự tồn tại của người dùng. Nếu không tìm thấy người dùng nào, chúng tôi sẽ phản hồi bằng mã trạng thái 400 cùng với thông báo , tương tự như hành vi trong . Invalid email or password signupController Nếu người dùng tồn tại, chúng tôi tiến hành so sánh mật khẩu được cung cấp với mật khẩu băm được lưu trữ trong cơ sở dữ liệu bằng cách sử dụng . bcrypt.compare Nếu mật khẩu không khớp, chúng tôi sẽ trả lời bằng thông báo quen thuộc 'Email hoặc mật khẩu không hợp lệ'. Cuối cùng, sau khi xác thực thành công, chúng tôi tạo mã thông báo, đặt phiên trong Redis và gửi lại mã thông báo cho khách hàng. Hãy xem lại bộ điều khiển được bảo vệ của chúng tôi, bộ điều khiển này phụ thuộc vào sự hiện diện của user_id thu được từ phần mềm trung gian. Chúng tôi luôn dựa vào user_id này để thực hiện các hoạt động trong các bộ điều khiển này. Trong trường hợp yêu cầu thiếu tiêu đề , chúng tôi phải phản hồi bằng mã trạng thái . authorization 401 const authHeader = req.headers['authorization']; Tạo tệp với đoạn mã sau: src/controllers/user/logoutController.ts import type { Request, Response } from 'express'; import { Redis } from 'src/redis'; export async function logoutController(req: Request, res: Response) { try { await Redis.deleteSession(req.user.id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } } Bộ điều khiển đăng xuất này chịu trách nhiệm đăng xuất người dùng khỏi hệ thống. Khi nhận được yêu cầu, nó sẽ tương tác với ứng dụng khách Redis để xóa phiên được liên kết với Nếu thao tác thành công, nó sẽ phản hồi bằng mã trạng thái để cho biết đăng xuất thành công. logoutController user.id 200 Tuy nhiên, nếu xảy ra lỗi trong quá trình, nó sẽ phản hồi bằng mã trạng thái để báo hiệu lỗi máy chủ nội bộ. 500 Tiếp theo, hãy giải quyết việc xóa dữ liệu người dùng. Tạo và thêm mã này: src/controllers/user/deleteUserController.ts import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export const deleteUserController = async (req: Request, res: Response) => { const user_id = req.user.id; try { await Redis.deleteSession(user_id); await UserModel.delete(user_id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } }; Xóa bộ điều khiển người dùng Khi nhận được yêu cầu, nó sẽ trích xuất ID người dùng từ đối tượng yêu cầu, thường được lấy từ phần mềm trung gian xác thực. Sau đó, nó tiến hành xóa phiên được liên kết với này khỏi Redis bằng ứng dụng khách Redis. Sau đó, nó gọi phương thức của để xóa dữ liệu của người dùng khỏi cơ sở dữ liệu. user_id delete UserModel Sau khi xóa thành công cả phiên và dữ liệu người dùng, nó sẽ phản hồi bằng mã trạng thái để cho biết việc xóa thành công. Trong trường hợp xảy ra lỗi trong quá trình xóa, nó sẽ phản hồi bằng mã trạng thái để biểu thị lỗi máy chủ nội bộ. 200 500 Để cập nhật dữ liệu người dùng trong hệ thống, hãy tạo và thêm mã sau vào tệp: src/controllers/user/updateUserController.ts import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { filterObject } from 'src/utils/filterObject'; type Payload = { first_name?: string; last_name?: string; username?: string; }; export const updateUserController = async (req: Request, res: Response) => { const { first_name, last_name, username } = req.body; const payload: Payload = filterObject({ first_name, last_name, username, }); try { const existingUserName = await UserModel.findByUsername(username); if (existingUserName) { return res.status(400).json({ error: 'Invalid username', }); } const updatedUser = await UserModel.updateOneById<typeof payload>(req.user.id, payload); res.status(200).json(updatedUser); } catch (error) { res.sendStatus(500); } }; Cập nhật bộ điều khiển người dùng Khi nhận được yêu cầu, nó sẽ trích xuất các trường , và từ nội dung yêu cầu. Tiếp theo, nó lọc các trường này bằng hàm tiện ích để đảm bảo rằng chỉ những trường hợp lệ mới được đưa vào tải trọng. first_name last_name username filterObject Sau đó, nó sẽ kiểm tra xem được cung cấp đã tồn tại trong cơ sở dữ liệu hay chưa. Nếu đúng như vậy, bộ điều khiển sẽ phản hồi bằng mã trạng thái và thông báo lỗi cho biết không hợp lệ. Nếu là duy nhất, bộ điều khiển sẽ tiến hành cập nhật dữ liệu người dùng trong cơ sở dữ liệu bằng phương thức của . username 400 username username updateOneById UserModel Sau khi cập nhật thành công, nó sẽ phản hồi bằng mã trạng thái và dữ liệu người dùng được cập nhật. Trong trường hợp có bất kỳ lỗi nào trong quá trình cập nhật, bộ điều khiển sẽ phản hồi bằng mã trạng thái để biểu thị lỗi máy chủ nội bộ. 200 500 Bước cuối cùng sẽ là cập nhật mật khẩu, ý tưởng gần giống như cập nhật dữ liệu người dùng nhưng có băm mật khẩu mới. Tạo bộ điều khiển cuối cùng từ danh sách của chúng tôi và thêm mã: src/controllers/user/updatePasswordController.ts import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import bcrypt from 'bcrypt'; export const updatePasswordController = async (req: Request, res: Response) => { try { const { password } = req.body; if (!password) return res.sendStatus(400); const hashedPassword = (await bcrypt.hash(password, 10)) as string; const user = await UserModel.updateOneById(req.user.id, { password: hashedPassword }); return res.status(200).json({ id: user.id }); } catch (error) { return res.sendStatus(500); } }; Cập nhật bộ điều khiển mật khẩu Khi nhận được yêu cầu, nó sẽ trích xuất mật khẩu mới từ nội dung yêu cầu. Sau đó nó sẽ kiểm tra xem mật khẩu có được cung cấp trong phần thân yêu cầu hay không. Nếu không, nó sẽ phản hồi bằng mã trạng thái , cho biết yêu cầu không hợp lệ. Tiếp theo, nó băm mật khẩu mới bằng thư viện với hệ số muối là 10. 400 bcrypt Sau đó, mật khẩu băm được lưu trữ an toàn trong cơ sở dữ liệu bằng phương thức của , liên kết nó với Sau khi cập nhật mật khẩu thành công, bộ điều khiển sẽ phản hồi bằng mã trạng thái và đối tượng JSON chứa ID người dùng. updateOneById UserModel user.id 200 Trong trường hợp có bất kỳ lỗi nào trong quá trình cập nhật mật khẩu, bộ điều khiển sẽ phản hồi bằng mã trạng thái để cho biết lỗi máy chủ nội bộ như trong các bộ điều khiển khác. 500 Đảm bảo xem xét và thiết lập trình trợ giúp xác thực và các tiện ích từ . Sau khi định cấu hình, bạn sẽ sẵn sàng kiểm tra các điểm cuối. Kho lưu trữ GitHub Hãy kiểm tra điểm cuối đăng ký: Rõ ràng là chúng tôi đã nhận được mã thông báo, mã này sẽ được sử dụng trong tiêu đề để truy xuất phiên. Chúng tôi đã gửi mã thông báo ủy quyền trong tiêu đề đến máy chủ và để đáp lại, máy chủ đã cung cấp cho chúng tôi dữ liệu người dùng được truy xuất từ cơ sở dữ liệu. Hãy thoải mái khám phá và thử nghiệm các tính năng bảo mật và bộ nhớ đệm Redis. Với mô hình nền tảng hiện có, bạn có thể nghiên cứu sâu hơn các chức năng bổ sung, chẳng hạn như khôi phục tài khoản cho người dùng quên mật khẩu. Tuy nhiên, chủ đề này sẽ được dành cho một bài viết trong tương lai. Phần kết luận Việc quản lý luồng định tuyến và xác thực người dùng theo cách có thể mở rộng có thể là một thách thức. Mặc dù chúng tôi đã triển khai phần mềm trung gian để bảo vệ các tuyến đường nhưng vẫn có các chiến lược bổ sung để nâng cao hiệu suất và độ tin cậy của dịch vụ. Trải nghiệm người dùng được nâng cao hơn nữa bằng cách cung cấp các thông báo lỗi rõ ràng hơn vì việc xử lý lỗi vẫn là một khía cạnh quan trọng đòi hỏi phạm vi bảo hiểm toàn diện hơn. Tuy nhiên, chúng tôi đã triển khai thành công luồng xác thực chính, cho phép người dùng đăng ký, truy cập tài khoản của họ, truy xuất dữ liệu phiên, cập nhật thông tin người dùng và xóa tài khoản. Tôi hy vọng bạn thấy hành trình này thật sâu sắc và thu được kiến thức có giá trị về xác thực người dùng. Tài nguyên Kho lưu trữ GitHub Knex.js Thể hiện Tạo ứng dụng nút Người phát thơ Cũng được xuất bản ở đây