paint-brush
Cách làm chủ xác thực và luồng người dùng trong Node.js với Knex và Redistừ tác giả@antonkalik
630 lượt đọc
630 lượt đọc

Cách làm chủ xác thực và luồng người dùng trong Node.js với Knex và Redis

từ tác giả Anton Kalik26m2024/03/17
Read on Terminal Reader

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

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. Tạo và hiểu giải pháp xác thực mạnh mẽ cho máy chủ Node js bằng Knex và Redis. Sử dụng Knex để tạo cơ sở dữ liệu cho Node.js, sau đó dùng Redis để lưu vào bộ đệm và Express để định tuyến dữ liệu.
featured image - Cách làm chủ xác thực và luồng người dùng trong Node.js với Knex và Redis
Anton Kalik HackerNoon profile picture
0-item

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à user-flow-boilerplate . Hãy tạo thư mục đó và chạy npm init -y bên trong để tạo package.json cơ bản


 { "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 src và đặt tệp điểm nhập ban đầu của chúng ta, index.ts , trong đó.

 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 typscript , lint , jest , bable , prettier , nodemon . Tất cả những tệp đó tôi đã mô tả trong bài viết sau: 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 npm run dev sẽ khởi động máy chủ và bạn sẽ thấy đầu ra tương tự như sau:

 ./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 Người phát thơ 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 GET mới, nhấn cmd + E (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à health .


Thêm nhập cho URL: {{BASE_URI}}/health . Đối với BASE_URI , 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: http://localhost:9999/api/v1

Người đưa thư đặt Url cơ sở

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 docker-compose . Để 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ư pgAdmin .


Cá nhân tôi thích sử dụng hơn RubyMỏ , đượ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ả.


Chúng tôi cần tệp .env với các khóa, mật khẩu và tên kiểm tra cần thiết:

 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="[email protected]" 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 JWT_SECRET để 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 docker-compose.yml ở thư mục gốc của dự án:

 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ụ đó docker-compose up và chúng ta phải có thể thấy đầu ra sau docker ps đầu ra sau:

 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 src/@types/index.ts nơi chúng ta lưu trữ các loại của mình cho ứng dụng:

 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ó knexfile.ts 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.


Tôi đã để lại lời giải thích khá chi tiết trong bài viết Tạo máy chủ Node.js bằng Postgres và Knex trên Express 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ô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 psql . Đượ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 knex migrate:latest . Sau đó, bạn nên quan sát các cột mới được thêm vào trong bảng users trong cơ sở dữ liệu.

Bảng người dùng sau khi di chuyển

Hãy gieo nó với dữ liệu giả knex seed:run và kiểm tra lại bảng.

Kết quả trong cơ sở dữ liệu sau hạt giống

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 src/routes/index.ts , hãy thêm đoạn mã sau:

 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 /health 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 get trước đó.

 -> 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 health src/controllers/healthController.ts bằng mã:

 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: authRouter.tsuserRouter.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.


Bộ điều khiển


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 bài báo , Tôi sẽ đưa mô hình cơ sở vào đây để dễ hiểu và dễ hiểu hơn. Tạo trong 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 UserModel.ts trong cùng một thư mục:

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

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 src/middlewares và thêm vào đó một tệp authMiddleware.ts với mã sau:

 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 ( req.user ) và gọi hàm next() để 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.

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 src/utils/jwt.ts với mã sau:

 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 jwt 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 jsonwebtoken . 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.


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 src/redis/index.ts với đoạn mã sau:

 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à sessionController.ts phải nằm trong src/controllers với mã sau:

 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:


Thông tin phiên người dùng: 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.


Xác thực và ủy quyền: Bằng cách kiểm tra xem req.user 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.


Truy xuất hồ sơ người dùng: Bộ điều khiển truy vấn cơ sở dữ liệu (sử dụng UserModel ) để 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:

 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ố CACHE_EXPIRATION để 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.


Trước khi tiếp tục tạo signUpController , 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 đồ:

Sơ đồ quy trình đăng ký

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 src/controllers/auth/signUpController.ts và thêm mã sau:

 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 userSchema được xác định trước để đảm bảo dữ liệu đáp ứng định dạng được yêu cầu.


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 bcrypt.hash , tạo username và tạo người dùng bằng cách sử dụng UserModel.create .


Cuối cùng, nó tạo token bằng cách sử dụng jwt , đặt dữ liệu session trong Redis và gửi lại token cho người dùng.


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 Invalid email or password , tương tự như hành vi trong 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 đề authorization , chúng tôi phải phản hồi bằng mã trạng thái 401 .


 const authHeader = req.headers['authorization'];


Tạo tệp src/controllers/user/logoutController.ts với đoạn mã sau:


 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


logoutController 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 user.id Nếu thao tác thành công, nó sẽ phản hồi bằng mã trạng thái 200 để cho biết đăng xuất thành công.


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 500 để báo hiệu lỗi máy chủ nội bộ.


Tiếp theo, hãy giải quyết việc xóa dữ liệu người dùng.


Tạo src/controllers/user/deleteUserController.ts và thêm mã này:

 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 user_id này khỏi Redis bằng ứng dụng khách Redis. Sau đó, nó gọi phương thức delete của UserModel để xóa dữ liệu của người dùng khỏi cơ sở dữ liệu.


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 200 để 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 500 để biểu thị lỗi máy chủ nội bộ.


Để cập nhật dữ liệu người dùng trong hệ thống, hãy tạo src/controllers/user/updateUserController.ts và thêm mã sau vào tệp:

 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 first_name , last_nameusername 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 filterObject để đảm bảo rằng chỉ những trường hợp lệ mới được đưa vào tải trọng.


Sau đó, nó sẽ kiểm tra xem username đượ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 400 và thông báo lỗi cho biết username không hợp lệ. Nếu username 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 updateOneById của UserModel .


Sau khi cập nhật thành công, nó sẽ phản hồi bằng mã trạng thái 200 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 500 để biểu thị lỗi máy chủ nội bộ.


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 src/controllers/user/updatePasswordController.ts và thêm mã:


 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 400 , 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 bcrypt với hệ số muối là 10.


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 updateOneById của UserModel , liên kết nó với user.id 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 200 và đối tượng JSON chứa ID người dùng.


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 500 để cho biết lỗi máy chủ nội bộ như trong các bộ điều khiển khác.


Đả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ừ Kho lưu trữ GitHub . Sau khi định cấu hình, bạn sẽ sẵn sàng kiểm tra các điểm cuối.


Hãy kiểm tra điểm cuối đăng ký:


Điểm đăng ký xác thực


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.


Kết quả của phiên phản hồi


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