paint-brush
Как освоить аутентификацию и поток пользователей в Node.js с помощью Knex и Redisк@antonkalik
901 чтения
901 чтения

Как освоить аутентификацию и поток пользователей в Node.js с помощью Knex и Redis

к Anton Kalik26m2024/03/17
Read on Terminal Reader

Слишком долго; Читать

Использование Knex для управления базами данных, Redis для эффективного кэширования и Express для плавной маршрутизации. Создайте и изучите надежное решение аутентификации для сервера Node js с использованием Knex и Redis. Используйте Knex для создания базы данных для Node.js, затем Redis для кэширования и Express для маршрутизации данных.
featured image - Как освоить аутентификацию и поток пользователей в Node.js с помощью Knex и Redis
Anton Kalik HackerNoon profile picture
0-item

Создайте надежное решение для аутентификации для сервера Node js, используя Knex для управления базами данных, Redis для эффективного кэширования и Express для плавной маршрутизации.

В поисках быстрого, интуитивно понятного и оптимизированного решения для аутентификации для моих приложений Node.js я столкнулся со сценариями, требующими быстрой реализации без ущерба для функциональности.


От регистрации пользователей и входа в систему до управления забытыми паролями, обновления пользовательских данных и даже удаления учетной записи — я искал комплексное решение, которое легко справляется с этими важными взаимодействиями с пользователем.


Таким образом, моя статья призвана представить именно это — целостный подход, объединяющий четкие методологии реализации аутентификации и кэширования, обеспечивающий надежный и эффективный пользовательский поток.


Здесь мы обойдем фундаментальные процедуры установки и создания модели, сосредоточившись непосредственно на тонкостях аутентификации и пользовательском потоке. Мы добавим в статью все необходимые ссылки для получения файлов конфигурации, обеспечивая беспрепятственный доступ к ресурсам, необходимым для установки.

Инструменты

Для этой реализации мы будем использовать Node.js версии 20.11.1 наряду с Knex, Express и Redis. Кроме того, мы будем использовать PostgreSQL в качестве нашей базы данных, которая будет помещена в контейнер и оркестрована с помощью Docker для обеспечения беспрепятственного управления.


Имя нашего приложения будет 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" }

Начальный пакет.json


Следующим шагом будет добавление необходимых зависимостей:


зависимости : npm i -S bcrypt body-parser cors dotenv express jsonwebtoken knex pg redis validator


Зависимости разработчика :

 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


и добавим скрипты, которые будут собирать и запускать наше приложение:

 "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" },


Чтобы обеспечить плавный запуск нашего приложения, необходимо создать папку 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); } })();

Файл точки входа


Для разработки нам необходимо иметь настройки для typscript , lint , jest , bable , prettier , nodemon . Все эти файлы я описал в следующей статье: Создание сервера Node.js с помощью Postgres и Knex в Express .


После настройки всех параметров и создания точки входа выполнение 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


Далее перейдите к Почтальон где мы создадим коллекцию, предназначенную для тестирования наших конечных точек. В новой коллекции добавьте новый запрос GET , нажмите cmd + E (на Mac, но клавиши зависят от вашей ОС) и назовите его health .


Добавьте ввод для URL: {{BASE_URI}}/health . Для BASE_URI добавьте новую переменную, которую вы собираетесь использовать в коллекции: http://localhost:9999/api/v1

Почтальон установил базовый URL-адрес

После этого просто нажмите кнопку «Отправить», и вы увидите текст ответа:

 { "message": "OK" }


База данных

Прежде чем двигаться дальше, крайне важно, чтобы наша база данных работала. Мы сделаем это, запустив его с помощью docker-compose . Для доступа к базе данных и управления ею вы можете использовать различные платформы разработки, такие как pgAdmin .


Лично я предпочитаю использовать РубиМайн , который оснащен драйвером, обеспечивающим плавное подключение к базам данных PostgreSQL для эффективного управления.


Нам нужен файл .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="[email protected]" TEST_USERNAME="test_username" TEST_PASSWORD="SomeParole999" # Redis REDIS_HOST="localhost" REDIS_PORT=6379 REDIS_DB=0 REDIS_PASSWORD="SomeParole999"

.env для подключения к базе данных, Redis и тестовых значений для начальных значений.


Не бойтесь, я случайно сгенерировал 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

docker-compose файл со службами


Мы собираемся запустить два сервиса в Docker для быстрого подключения. Я упростил этот процесс, чтобы облегчить быстрый доступ к базе данных или Redis, что позволяет нам эффективно извлекать данные. Итак, давайте запустим эти службы 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


Теперь нам нужно создать файл 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; }

Типы для обслуживания


На данный момент вам необходимо иметь knexfile.ts в корне проекта и папке базы данных для подключения, миграции и семян.


Я оставил довольно подробное объяснение в статье «Создание сервера Node.js с помощью Postgres и Knex on Express» о том, как мигрировать и заполнять пользователей в базе данных, где мы используем эти переменные env.


Я хотел бы специально проверить миграции, чтобы убедиться, что мы находимся на одной странице. Мы уже запустили наши сервисы, и надо иметь возможность проверить подключение к базе данных.

 docker exec -it postgres psql -U username_123 user_flow_boilerplate


Если соединение хорошее, вы окажетесь в консоли psql . Хорошо, если с соединением проблем нет, то мы сможем перенести туда наши таблицы. Запустите knex migrate:latest . Затем вы должны наблюдать за вновь добавленными столбцами в таблице users в базе данных.

Таблица пользователей после миграции

Давайте заполним его поддельными данными knex seed:run и еще раз проверим таблицу.

Результат в базе данных после заполнения

Итак, теперь мы готовы манипулировать базой данных, что позволяет нам добавлять, удалять или обновлять пользователей по мере необходимости.

Маршрутизатор

Наконец, мы можем забыть о настройках и подготовке и сосредоточиться конкретно на пользовательском потоке. Для этого нам нужно создать маршрутизатор. Нам нужно выполнить с помощью этого маршрутизатора следующие операции: login в систему, logout , signup , delete_user , update_user .


Для этого в 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' }); });

Файл маршрутов


Как видите, вначале мы добавили маршрут /health , который уже проверили. Итак, давайте обновим точку входа, чтобы применить к ней эти маршруты. Сначала удалите предыдущий get .

 -> REMOVE -> app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' }));


и добавьте в начало файла:

 import { router } from 'src/routes'; // ... app.use(cors()); app.use('/api/v1', router);


и создайте первый контроллер для проверки health src/controllers/healthController.ts с кодом:

 import { Request, Response } from 'express'; export const healthController = (_: Request, res: Response) => res.status(200).send('ok');

Контролер здоровья


Теперь вернемся к маршрутизатору и проверим, что еще нам нужно добавить в маршруты. Нам нужно добавить еще два файла: 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);

Авторизация Маршрутизатор


 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);

Пользовательский маршрутизатор


Я разделил эту логику ради удобства чтения и ответственности за поддержку изолированной функциональности. Всем этим маршрутам нужны контроллеры, в которых мы будем обрабатывать логику.


Контроллеры


Маршруты аутентификации и работоспособности не нуждаются в промежуточном программном обеспечении аутентификации, поэтому эти маршруты не защищены, но если совпадений нет, мы получим статус 404.

 router.get('/health', healthController); router.use('/auth', authRouter);

Теперь, когда мы определили все маршруты, нам нужно установить модель пользователя.

Модель пользователя

Я буду использовать базовую модель для модели пользователя, из которой я буду повторно использовать методы CRUD. Хотя я ранее рассматривал создание моделей в другом статья , я включу сюда базовую модель для лучшей наглядности и понимания. Создайте в 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(); } }

Базовая модель


С базовой моделью мы должны иметь возможность создать 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 }); } }

Модель пользователя


В модели пользователя я устанавливаю role по умолчанию, если она не указана в полезной нагрузке. И теперь, когда у нас есть готовые модели, мы можем приступить к их использованию в наших контроллерах и промежуточном программном обеспечении.

Промежуточное ПО для аутентификации

Промежуточное программное обеспечение аутентификации в приложении Node.js отвечает за аутентификацию входящих запросов, гарантируя, что они поступают от действительных и авторизованных пользователей.


Обычно он перехватывает входящие запросы, извлекает токены аутентификации или учетные данные и проверяет их достоверность с помощью предопределенного механизма аутентификации, такого как в данном случае JWT (веб-токены JSON).


Если процесс аутентификации успешен, промежуточное программное обеспечение позволяет запросу перейти к следующему обработчику в цикле запрос-ответ. Однако если аутентификация не удалась, он отвечает соответствующим кодом состояния HTTP (например, 401 Неавторизованный) и, при необходимости, выдает сообщение об ошибке.


Создайте папку 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); } }

Файл промежуточного программного обеспечения аутентификации


Промежуточное программное обеспечение аутентификации извлекает токен JWT из заголовка запроса, проверяет его достоверность с помощью библиотеки JWT и проверяет, соответствует ли токен токену, хранящемуся в Redis.


Если токен действителен и соответствует сохраненному токену, промежуточное программное обеспечение устанавливает сеанс аутентифицированного пользователя для объекта запроса ( req.user ) и вызывает функцию next() для передачи управления следующему промежуточному программному обеспечению или обработчику маршрута. В противном случае он отвечает кодом состояния 401, указывающим на сбой аутентификации.

Веб-токены JSON

Давайте рассмотрим утилиту для jwt. Создайте в файле 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); } }); }, };

Утилитный файл JWT


Эта утилита играет важную роль в обработке веб-токенов JSON в приложении Node.js. Объект jwt экспортирует функции для подписания и проверки JWT, используя библиотеку jsonwebtoken . Эти функции облегчают создание и проверку JWT, что необходимо для реализации механизмов аутентификации в приложении.


Утилита инкапсулирует функциональные возможности обработки JWT, обеспечивая безопасные механизмы аутентификации в приложении Node.js и придерживаясь лучших практик управления переменными среды.

Редис

Используется в качестве базы данных, кэша и брокера сообщений. Обычно используется в различных случаях, включая кэширование, управление сеансами, аналитику в реальном времени, очереди сообщений, таблицы лидеров и многое другое.


Проверка токена из Redis служит дополнительным уровнем безопасности и проверки токена JWT. Давайте углубимся в настройки. Для этого создайте файл 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 };

Хранилище сеансов Redis


С помощью Redis мы собираемся хранить токены пользовательских сеансов и управлять ими. В промежуточном программном обеспечении аутентификации после проверки подлинности токена JWT оно проверяет, существует ли токен и соответствует ли он тому, который хранится в Redis для соответствующего сеанса пользователя. Это помогает гарантировать, что только действительные и авторизованные пользователи смогут получить доступ к защищенным маршрутам.


Redis используется как хранилище значений ключей для хранения токенов сеанса пользователя. Когда пользователь входит в систему или проходит проверку подлинности, его токен сеанса сохраняется в Redis. Это позволяет эффективно и быстро извлекать токены сеанса во время последующих проверок аутентификации.


Redis используется в промежуточном программном обеспечении аутентификации для эффективного управления сеансами, в то время как файл, связанный с Redis, управляет настройкой и подключением к серверу Redis, а также предоставляет функции для взаимодействия с Redis в других частях приложения.


Такая настройка обеспечивает безопасные и надежные механизмы аутентификации, при этом токены сеансов пользователей хранятся и управляются в Redis.


Последняя часть: нам нужно подключиться к Redis в нашей точке входа:

 // 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); } })();

Подключиться к Редису


После завершения подготовки к аутентификации мы можем переключить внимание на контроллеры.

Контроллеры

Контроллеры в маршрутах помогают организовать логику приложения, разделяя задачи и обеспечивая удобство сопровождения кода. Мы уже создали контроллер для проверки работоспособности. Далее мы приступим к созданию контроллеров для обработки операций с пользователем.


Первый контроллер, который мы возьмем, — это 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); } };

Контроллер сеансов


Этот контроллер служит для обработки конечной точки, связанной с сеансом, вероятно, отвечающей за получение информации о текущем аутентифицированном пользователе. Этот контроллер нам нужен по следующим причинам:


Информация о сеансе пользователя: этот контроллер позволяет приложению получать информацию о сеансе пользователя, например его профиль пользователя или другие соответствующие данные. Эта информация может быть полезна для настройки взаимодействия с пользователем или предоставления персонализированного контента на основе профиля пользователя.


Аутентификация и авторизация. Проверяя наличие req.user , контроллер гарантирует, что только прошедшие проверку подлинности пользователи смогут получить доступ к конечной точке. Это помогает обеспечить соблюдение правил аутентификации и авторизации, гарантируя, что конфиденциальные пользовательские данные будут доступны только авторизованным пользователям.


Получение профиля пользователя: контроллер запрашивает базу данных (используя UserModel ), чтобы получить информацию о пользователе на основе его идентификатора сеанса. Это позволяет приложению динамически получать данные, специфичные для пользователя, обеспечивая индивидуальный подход для каждого пользователя. Эту часть определенно можно улучшить с помощью кэша 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); } };

Файл контроллера сеансов с установленным сеансом Redis


Мы определяем константу CACHE_EXPIRATION , чтобы указать время истечения срока действия кэша в секундах. В этом примере установлено значение 3600 секунд (1 час). Кэшированные данные периодически обновляются, что предотвращает передачу устаревших данных пользователям и обеспечивает целостность данных в кеше.


Прежде чем приступить к созданию signUpController , который управляет процессом регистрации новых пользователей в нашем приложении, давайте рассмотрим схему:

Схема процесса регистрации

В нашем случае, пытаясь зарегистрироваться с помощью существующего в базе данных адреса электронной почты, мы уделяем приоритетное внимание конфиденциальности пользователя, не раскрывая явным образом, существует ли пользователь. Вместо этого мы информируем клиента общим сообщением о Invalid email or password .


Этот подход побуждает клиента предоставлять действительные учетные данные, не раскрывая ненужную информацию о существующих пользователях.


Теперь давайте создадим 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); } }

Зарегистрироваться Контроллер


Контроллер получает запрос, содержащий адрес электронной почты и пароль пользователя, обычно из формы регистрации. Он проверяет входящие данные на соответствие предопределенной userSchema , чтобы убедиться, что они соответствуют требуемому формату.


Если проверка проходит успешно, указывая на отсутствие существующего пользователя и допустимых полей, контроллер приступает к хэшированию пароля с помощью bcrypt.hash , генерирует username и создает пользователя с помощью UserModel.create .


Наконец, он генерирует token с помощью jwt , устанавливает данные session в Redis и отправляет token обратно пользователю.


Теперь давайте сосредоточимся на создании контроллера входа. Создайте файл 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); } }

Контроллер входа


По сути, мы начинаем с проверки предоставленных полей, а затем проверяем существование пользователя. Если ни один пользователь не найден, мы отвечаем кодом состояния 400 вместе с сообщением Invalid email or password , аналогично поведению в signupController .


Если пользователь существует, мы приступаем к сравнению предоставленного пароля с хешированным паролем, хранящимся в базе данных, с помощью bcrypt.compare .


Если пароли не совпадают, мы отвечаем знакомым сообщением «Неверный адрес электронной почты или пароль». Наконец, после успешной аутентификации мы генерируем токен, устанавливаем сеанс в Redis и отправляем токен обратно клиенту.


Давайте рассмотрим наши защищенные контроллеры, которые зависят от наличия user_id, полученного из промежуточного программного обеспечения. Мы постоянно полагаемся на этот user_id для операций внутри этих контроллеров. В тех случаях, когда в запросе отсутствует заголовок authorization , мы должны ответить кодом состояния 401 .


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


Создайте файл 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); } }

Выход из контроллера


Этот logoutController отвечает за выход пользователя из системы. Получив запрос, он взаимодействует с клиентом Redis, чтобы удалить сеанс, связанный с user.id Если операция прошла успешно, он отвечает кодом состояния 200 , указывающим на успешный выход из системы.


Однако если во время процесса возникает ошибка, он отвечает кодом состояния 500 , сигнализирующим о внутренней ошибке сервера.


Далее займемся удалением пользовательских данных.


Создайте 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); } };

Удалить пользовательский контроллер


При получении запроса он извлекает идентификатор пользователя из объекта запроса, обычно получаемого из промежуточного программного обеспечения аутентификации.


Впоследствии он приступает к удалению сеанса, связанного с этим user_id из Redis с помощью клиента Redis. После этого он вызывает метод delete UserModel , чтобы удалить данные пользователя из базы данных.


При успешном удалении сеанса и пользовательских данных он отвечает кодом состояния 200 , указывающим на успешное удаление. В случае ошибки во время процесса удаления он отвечает кодом состояния 500 , что указывает на внутреннюю ошибку сервера.


Чтобы обновить данные пользователя в системе, создайте 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); } };

Обновить пользовательский контроллер


При получении запроса он извлекает поля first_name , last_name и username из тела запроса. Затем он фильтрует эти поля с помощью служебной функции filterObject , чтобы гарантировать, что в полезную нагрузку включены только допустимые поля.


Впоследствии он проверяет, существует ли указанное username в базе данных. Если это так, контроллер отвечает кодом состояния 400 и сообщением об ошибке, указывающим на неверное username . Если username уникально, контроллер приступает к обновлению данных пользователя в базе данных с помощью метода updateOneById UserModel .


При успешном обновлении он отвечает кодом состояния 200 и обновленными пользовательскими данными. В случае каких-либо ошибок во время процесса обновления контроллер отвечает кодом состояния 500 , что указывает на внутреннюю ошибку сервера.


Последним будет обновление пароля, идея практически аналогична обновлению пользовательских данных, но с хешированием нового пароля. Создайте последний контроллер из нашего списка 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); } };

Обновить контроллер паролей


При получении запроса он извлекает новый пароль из тела запроса. Затем он проверяет, указан ли пароль в теле запроса. Если нет, он отвечает кодом состояния 400 , указывающим на неправильный запрос. Затем он хэширует новый пароль с помощью библиотеки bcrypt с коэффициентом соли 10.


Затем хешированный пароль надежно сохраняется в базе данных с помощью метода updateOneById UserModel , связывающего его с user.id После успешного обновления пароля контроллер отвечает кодом состояния 200 и объектом JSON, содержащим идентификатор пользователя.


В случае каких-либо ошибок во время процесса обновления пароля контроллер отвечает кодом состояния 500 , указывающим на внутреннюю ошибку сервера, как и в других контроллерах.


Обязательно просмотрите и настройте помощник проверки и утилиты из Репозиторий GitHub . После настройки вы должны быть готовы протестировать конечные точки.


Давайте проверим конечную точку регистрации:


Точка регистрации аутентификации


Как видно, мы получили токен, который будет использоваться в заголовке для получения сеанса.


Результат сессии в ответ


Мы отправили токен авторизации в заголовке на сервер, а в ответ сервер предоставил нам данные пользователя, полученные из базы данных.


Не стесняйтесь исследовать и экспериментировать с функциями безопасности и кэшированием Redis. Имея базовую модель, вы можете углубиться в дополнительные функции, такие как восстановление учетной записи для пользователей, которые забывают свои пароли. Однако эта тема будет зарезервирована для следующей статьи.

Заключение

Масштабируемое управление потоками маршрутизации и аутентификации пользователей может оказаться сложной задачей. Хотя мы внедрили промежуточное программное обеспечение для защиты маршрутов, существуют дополнительные стратегии для повышения производительности и надежности службы.


Пользовательский опыт еще больше повышается за счет предоставления более четких сообщений об ошибках, поскольку обработка ошибок остается важным аспектом, требующим более полного освещения. Однако мы успешно реализовали основной поток аутентификации, позволяющий пользователям регистрироваться, получать доступ к своим учетным записям, получать данные сеанса, обновлять информацию о пользователях и удалять учетные записи.


Я надеюсь, что это путешествие оказалось для вас познавательным и вы получили ценные знания в области аутентификации пользователей.

Ресурсы

Репозиторий GitHub
Knex.js
Выражать
Создать приложение узла
Почтальон


Также опубликовано здесь