Erstellen Sie eine robuste Authentifizierungslösung für den Node-JS-Server mit Knex für die Datenbankverwaltung, Redis für effizientes Caching und Express für nahtloses Routing. Auf meiner Suche nach einer schnellen, intuitiven und optimierten Authentifizierungslösung für meine Node.js-Anwendungen stieß ich auf Szenarien, die eine schnelle Implementierung ohne Beeinträchtigung der Funktionalität erforderten. Von der Benutzerregistrierung und -anmeldung über die Verwaltung vergessener Passwörter, die Aktualisierung von Benutzerdaten bis hin zur Kontolöschung suchte ich nach einer umfassenden Lösung, die nahtlos durch diese wichtigen Benutzerinteraktionen navigiert. Daher zielt mein Artikel darauf ab, genau das darzustellen – einen kohärenten Ansatz, der klare Methoden zur Implementierung von Authentifizierung und Caching integriert und so einen robusten und effizienten Benutzerfluss gewährleistet. Hier umgehen wir die grundlegenden Installationsverfahren und die Modellerstellung und konzentrieren uns direkt auf die Feinheiten der Authentifizierung und des Benutzerflusses. Wir werden im gesamten Artikel alle notwendigen Links zum Abrufen von Konfigurationsdateien einfügen und so einen nahtlosen Zugriff auf die für die Einrichtung erforderlichen Ressourcen gewährleisten. Werkzeuge Für diese Implementierung nutzen wir Node.js Version 20.11.1 neben Knex, Express und Redis. Darüber hinaus nutzen wir PostgreSQL als unsere Datenbank, die für eine nahtlose Verwaltung mit Docker containerisiert und orchestriert wird. Der Name unserer Anwendung lautet . Lassen Sie uns diesen Ordner erstellen und darin ausführen, um die grundlegende zu generieren 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" } Ursprüngliches package.json Der nächste Schritt besteht darin, die notwendigen Abhängigkeiten hinzuzufügen: : Abhängigkeiten npm i -S bcrypt body-parser cors dotenv express jsonwebtoken knex pg redis validator : DevDependencies 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 und fügen Sie Skripte hinzu, die unsere Anwendung erstellen und ausführen: "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" }, Um den reibungslosen Start unserer Anwendung zu gewährleisten, ist es wichtig, einen Ordner zu erstellen und darin unsere anfängliche Einstiegspunktdatei, , zu platzieren. 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); } })(); Entrypoint-Datei Für die Entwicklung benötigen wir Einstellungen für , , , , und . Alle diese Dateien habe ich im folgenden Artikel beschrieben: . typscript lint jest bable prettier nodemon Erstellen eines Node.js-Servers mit Postgres und Knex auf Express Nachdem Sie alle Einstellungen konfiguriert und den Einstiegspunkt erstellt haben, sollte die Ausführung von den Server starten und Sie sollten mit einer Ausgabe wie der folgenden rechnen: 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 Navigieren Sie als Nächstes zu Hier werden wir eine Sammlung zum Testen unserer Endpunkte einrichten. Fügen Sie in der neuen Sammlung eine neue Anfrage hinzu, drücken Sie (auf dem Mac, aber die Tasten hängen von Ihrem Betriebssystem ab) und benennen Sie sie als . Briefträger GET cmd + E health Eingabe für URL hinzufügen: . Fügen Sie für eine neue Variable hinzu, die Sie in der gesamten Sammlung verwenden werden: {{BASE_URI}}/health BASE_URI http://localhost:9999/api/v1 Klicken Sie anschließend einfach auf die Schaltfläche „Senden“ und Sie sollten den Antworttext sehen: { "message": "OK" } Datenbank Bevor wir fortfahren, ist es wichtig, dass unsere Datenbank betriebsbereit ist. Dies erreichen wir, indem wir es mit starten. Um auf die Datenbank zuzugreifen und sie zu verwalten, können Sie verschiedene Entwicklungsplattformen wie verwenden . docker-compose pgAdmin Persönlich bevorzuge ich die Verwendung , das mit einem Treiber ausgestattet ist, der eine nahtlose Konnektivität zu PostgreSQL-Datenbanken für eine effiziente Verwaltung ermöglicht. RubyMine Wir benötigen Datei mit den erforderlichen Schlüsseln, Passwörtern und Testnamen: .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 für die Verbindung zur Datenbank, Redis und Testwerte für Seeds Keine Angst, ich habe zufällig generiert, um es authentischer zu veranschaulichen. Erstellen wir also eine Datei im Stammverzeichnis des Projekts: 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-Datei mit Diensten Wir werden in Docker zwei Dienste für eine schnelle Konnektivität einrichten. Ich habe diesen Prozess optimiert, um einen schnellen Zugriff auf die Datenbank oder Redis zu ermöglichen und es uns zu ermöglichen, Daten effizient abzurufen. Lassen Sie uns also diese Dienste ausführen, und wir müssen in der Lage sein, die Ausgabe nach der folgenden Ausgabe zu sehen: 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 Jetzt müssen wir die Datei erstellen, in der wir unsere Typen für die Anwendung speichern: 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; } Typen für den Service Zu diesem Zeitpunkt müssen Sie im Stammverzeichnis des Projekts und des Datenbankordners für Verbindungen, Migrationen und Seeds haben. knexfile.ts Ich habe im Artikel eine ziemlich ausführliche Erklärung hinterlassen, wie man Benutzer migriert und in die Datenbank einfügt, in der wir diese Umgebungsvariablen verwenden. „Erstellen eines Node.js-Servers mit Postgres und Knex in Express“ Ich möchte die Migrationen speziell überprüfen, um sicherzustellen, dass wir auf dem gleichen Stand sind. Wir haben unsere Dienste bereits gestartet und müssen in der Lage sein, die Verbindung zur Datenbank zu überprüfen. docker exec -it postgres psql -U username_123 user_flow_boilerplate Wenn die Verbindung gut ist, befinden Sie sich in der Konsole. Ok, wenn die Verbindung keine Probleme hat, sollten wir in der Lage sein, unsere Tabellen dorthin zu migrieren. Führen Sie aus. Dann sollten Sie die neu hinzugefügten Spalten in Ihrer in der Datenbank beobachten. psql knex migrate:latest users Lassen Sie es uns mit gefälschten Daten säen und die Tabelle noch einmal überprüfen. knex seed:run Damit sind wir nun in der Lage, die Datenbank zu manipulieren und Benutzer nach Bedarf hinzuzufügen, zu löschen oder zu aktualisieren. Router Schließlich können wir Einstellungen und Vorbereitung vergessen und uns speziell auf den Benutzerfluss konzentrieren. Dafür müssen wir einen Router erstellen. Wir müssen von diesem Router die folgenden Vorgänge verarbeiten: , , , , . login logout signup delete_user update_user Fügen Sie dazu auf den folgenden Code hinzu: 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' }); }); Routendatei Wie Sie sehen können, haben wir am Anfang die Route hinzugefügt, die wir bereits überprüft haben. Aktualisieren wir also den Einstiegspunkt, um diese Routen dort anzuwenden. Entfernen Sie zunächst das vorherige . /health get -> REMOVE -> app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); und am Anfang der Datei hinzufügen: import { router } from 'src/routes'; // ... app.use(cors()); app.use('/api/v1', router); und erstellen Sie den ersten Controller für mit dem Code: health src/controllers/healthController.ts import { Request, Response } from 'express'; export const healthController = (_: Request, res: Response) => res.status(200).send('ok'); Gesundheitskontrolleur Kommen wir nun zurück zum Router und prüfen wir, was wir den Routen noch hinzufügen müssen. Wir müssen zwei weitere Dateien hinzufügen: und 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); Authentifizierungsrouter 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); Benutzer-Router Ich habe diese Logik aus Gründen der Lesbarkeit und der Verantwortung für die Beibehaltung isolierter Funktionalität aufgeteilt. Alle diese Routen benötigen Controller, in denen wir die Logik verwalten. Authentifizierungs- und Integritätsrouten benötigen keine Authentifizierungs-Middleware, daher sind diese Routen nicht geschützt. Wenn es jedoch keine Übereinstimmung gibt, erhalten wir den Status 404. router.get('/health', healthController); router.use('/auth', authRouter); Nachdem wir nun alle Routen festgelegt haben, müssen wir das Benutzermodell festlegen. Benutzermodell Ich werde ein Basismodell für das Benutzermodell verwenden, von dem aus ich CRUD-Methoden wiederverwenden werde. Während ich die Modellerstellung bereits in einem anderen Artikel behandelt habe Zur besseren Sichtbarkeit und zum besseren Verständnis füge ich hier das Basismodell hinzu. Erstellen Sie in Artikel 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(); } } Basismodell Mit dem Basismodell müssen wir im selben Ordner erstellen können: 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 }); } } Benutzermodell Im Benutzermodell lege ich nur standardmäßig fest, wenn sie nicht aus der Nutzlast bereitgestellt wird. Und jetzt, da wir unsere Modelle fertig haben, können wir sie in unseren Controllern und Middlewares verwenden. role Authentifizierungs-Middleware Die Authentifizierungs-Middleware in einer Node.js-Anwendung ist für die Authentifizierung eingehender Anfragen verantwortlich und stellt sicher, dass sie von gültigen und autorisierten Benutzern stammen. Typischerweise fängt es eingehende Anfragen ab, extrahiert Authentifizierungstoken oder Anmeldeinformationen und überprüft deren Gültigkeit anhand eines vordefinierten Authentifizierungsmechanismus, wie in diesem Fall JWT (JSON Web Tokens). Wenn der Authentifizierungsprozess erfolgreich ist, ermöglicht die Middleware, dass die Anfrage an den nächsten Handler im Anfrage-Antwort-Zyklus weitergeleitet wird. Wenn die Authentifizierung jedoch fehlschlägt, antwortet es mit einem entsprechenden HTTP-Statuscode (z. B. 401 Nicht autorisiert) und gibt optional eine Fehlermeldung aus. Erstellen Sie den Ordner und fügen Sie dort eine Datei mit dem folgenden Code hinzu: 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); } } Auth-Middleware-Datei Die Authentifizierungs-Middleware extrahiert das JWT-Token aus dem Anforderungsheader, überprüft seine Gültigkeit mithilfe der JWT-Bibliothek und prüft, ob das Token mit dem in Redis gespeicherten Token übereinstimmt. Wenn das Token gültig ist und mit dem gespeicherten Token übereinstimmt, setzt die Middleware die authentifizierte Benutzersitzung auf das Anforderungsobjekt ( ) und ruft die Funktion auf, um die Steuerung an die nächste Middleware oder den nächsten Routenhandler zu übergeben. Andernfalls antwortet es mit dem Statuscode 401, der auf einen Authentifizierungsfehler hinweist. req.user next() JSON-Web-Tokens Sehen wir uns das Dienstprogramm für JWT an. Erstellen Sie die Datei mit dem folgenden Code: 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-Dienstprogrammdatei Dieses Dienstprogramm spielt eine entscheidende Rolle bei der Verarbeitung von JSON-Web-Tokens innerhalb der Node.js-Anwendung. Das Objekt exportiert Funktionen zum Signieren und Überprüfen von JWTs und nutzt dabei die Bibliothek. Diese Funktionen erleichtern die Erstellung und Validierung von JWTs, die für die Implementierung von Authentifizierungsmechanismen in der Anwendung unerlässlich sind. jwt jsonwebtoken Das Dienstprogramm kapselt die Funktionalität für die Handhabung von JWTs und gewährleistet sichere Authentifizierungsmechanismen innerhalb der Node.js-Anwendung unter Einhaltung bewährter Methoden für die Verwaltung von Umgebungsvariablen. Redis Wird als Datenbank, Cache und Nachrichtenbroker verwendet. Wird häufig in einer Vielzahl von Anwendungsfällen verwendet, darunter Caching, Sitzungsverwaltung, Echtzeitanalysen, Messaging-Warteschlangen, Bestenlisten und mehr. Die Überprüfung des Tokens von Redis dient als zusätzliche Sicherheits- und Validierungsebene für das JWT-Token. Lassen Sie uns in die Einstellungen eintauchen. Erstellen Sie dazu in der Datei den folgenden Code: 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-Sitzungsspeicher Mit Redis werden wir Benutzersitzungstoken speichern und verwalten. In der Authentifizierungs-Middleware prüft die Middleware nach der Überprüfung der Authentizität des JWT-Tokens, ob das Token vorhanden ist und mit dem in Redis für die entsprechende Benutzersitzung gespeicherten Token übereinstimmt. Dadurch wird sichergestellt, dass nur gültige und autorisierte Benutzer auf geschützte Routen zugreifen können. Redis wird als Schlüsselwertspeicher zur Verwaltung von Benutzersitzungstoken verwendet. Wenn sich ein Benutzer anmeldet oder authentifiziert, wird sein Sitzungstoken in Redis gespeichert. Dies ermöglicht einen effizienten und schnellen Abruf von Sitzungstoken bei nachfolgenden Authentifizierungsprüfungen. Redis wird in der Authentifizierungs-Middleware für eine effiziente Sitzungsverwaltung verwendet, während die Redis-bezogene Datei die Konfiguration und Verbindung zum Redis-Server übernimmt und Funktionen für die Interaktion mit Redis in anderen Teilen der Anwendung bereitstellt. Dieses Setup gewährleistet sichere und zuverlässige Authentifizierungsmechanismen, wobei Benutzersitzungstoken in Redis gespeichert und verwaltet werden. Der letzte Teil besteht darin, dass wir an unserem Einstiegspunkt eine Verbindung zu Redis herstellen müssen: // 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); } })(); Stellen Sie eine Verbindung zu Redis her Nach Abschluss der Authentifizierungsvorbereitung können wir uns nun auf die Controller konzentrieren. Controller Controller in Routen helfen bei der Organisation der Anwendungslogik, indem sie Bedenken trennen und die Wartbarkeit des Codes fördern. Wir haben den Controller für den Gesundheitscheck bereits erstellt. Als Nächstes erstellen wir Controller für die Abwicklung von Vorgängen mit dem Benutzer. Der erste Controller, den wir nehmen werden, ist , der sich in mit dem folgenden Code befinden muss: 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); } }; Sitzungscontroller Dieser Controller dient der Verwaltung eines sitzungsbezogenen Endpunkts, der wahrscheinlich für das Abrufen von Informationen über den aktuell authentifizierten Benutzer verantwortlich ist. Wir benötigen diesen Controller aus folgenden Gründen: Mit diesem Controller kann die Anwendung Informationen über die Sitzung des Benutzers abrufen, beispielsweise sein Benutzerprofil oder andere relevante Daten. Diese Informationen können nützlich sein, um die Benutzererfahrung anzupassen oder personalisierte Inhalte basierend auf dem Benutzerprofil bereitzustellen. Informationen zur Benutzersitzung: Durch die Prüfung, ob vorhanden ist, stellt der Controller sicher, dass nur authentifizierte Benutzer auf den Endpunkt zugreifen können. Dies hilft bei der Durchsetzung von Authentifizierungs- und Autorisierungsregeln und stellt sicher, dass vertrauliche Benutzerdaten nur autorisierten Benutzern zugänglich sind. Authentifizierung und Autorisierung: req.user Der Controller fragt die Datenbank ab (unter Verwendung des ), um die Informationen des Benutzers basierend auf seiner Sitzungs-ID abzurufen. Dadurch kann die Anwendung benutzerspezifische Daten dynamisch abrufen und so für jeden Benutzer ein maßgeschneidertes Erlebnis bieten. Dieser Teil kann durch den Redis-Cache definitiv verbessert werden: Benutzerprofilabruf: 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); } }; Session-Controller-Datei mit Redis-Set-Sitzung Wir definieren eine Konstante , um die Cache-Ablaufzeit in Sekunden anzugeben. In diesem Beispiel ist es auf 3600 Sekunden (1 Stunde) eingestellt. Zwischengespeicherte Daten werden regelmäßig aktualisiert, um zu verhindern, dass veraltete Daten an Benutzer bereitgestellt werden, und um die Datenintegrität im Cache aufrechtzuerhalten. CACHE_EXPIRATION Bevor wir mit der Erstellung des fortfahren, der den Anmeldevorgang für neue Benutzer in unserer Anwendung verwaltet, sehen wir uns das Schema an: signUpController Wenn wir in unserem Fall versuchen, uns mit einer in der Datenbank vorhandenen E-Mail-Adresse anzumelden, legen wir Wert auf die Privatsphäre des Benutzers, indem wir nicht explizit offenlegen, ob der Benutzer existiert. Stattdessen informieren wir den Kunden mit einer allgemeinen Nachricht mit der Meldung . Invalid email or password Dieser Ansatz ermutigt den Kunden, gültige Anmeldeinformationen einzureichen, ohne unnötige Informationen über bestehende Benutzer preiszugeben. Jetzt erstellen wir und fügen den folgenden Code hinzu: 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); } } Registrieren Sie sich als Controller Der Controller erhält eine Anfrage mit der E-Mail-Adresse und dem Passwort des Benutzers, normalerweise über ein Anmeldeformular. Es validiert die eingehenden Daten anhand eines vordefinierten , um sicherzustellen, dass sie dem erforderlichen Format entsprechen. userSchema Wenn die Validierung erfolgreich verläuft und kein vorhandener Benutzer und keine gültigen Felder angezeigt werden, fährt der Controller mit dem Hashen des Kennworts mithilfe von fort, generiert einen und erstellt den Benutzer mithilfe von . bcrypt.hash username UserModel.create Schließlich generiert es mithilfe von ein , legt die in fest und sendet das an den Benutzer zurück. jwt token session Redis token Konzentrieren wir uns nun auf die Erstellung eines Login-Controllers. Erstellen Sie die Datei : 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); } } Login-Controller Im Wesentlichen beginnen wir mit der Validierung der bereitgestellten Felder und prüfen dann, ob ein Benutzer vorhanden ist. Wenn kein Benutzer gefunden wird, antworten wir mit einem 400-Statuscode zusammen mit der Meldung , ähnlich dem Verhalten im . Invalid email or password signupController Wenn ein Benutzer vorhanden ist, vergleichen wir mit das bereitgestellte Passwort mit dem in der Datenbank gespeicherten Hash-Passwort. bcrypt.compare Wenn die Passwörter nicht übereinstimmen, antworten wir mit der bekannten Meldung „Ungültige E-Mail-Adresse oder ungültiges Passwort“. Schließlich generieren wir nach erfolgreicher Authentifizierung ein Token, richten die Sitzung in Redis ein und senden das Token an den Client zurück. Sehen wir uns unsere geschützten Controller an, die vom Vorhandensein einer von der Middleware erhaltenen user_id abhängig sind. Wir verlassen uns bei Vorgängen innerhalb dieser Controller konsequent auf diese user_id. In Fällen, in denen der Anfrage ein fehlt, müssen wir mit einem Statuscode antworten. authorization 401 const authHeader = req.headers['authorization']; Erstellen Sie die Datei mit dem folgenden Code: 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); } } Logout-Controller Dieser ist für die Abmeldung eines Benutzers vom System verantwortlich. Beim Empfang einer Anfrage interagiert es mit dem Redis-Client, um die mit der verknüpfte Sitzung zu löschen. Wenn der Vorgang erfolgreich ist, antwortet es mit dem Statuscode , um die erfolgreiche Abmeldung anzuzeigen. logoutController user.id 200 Sollte während des Vorgangs jedoch ein Fehler auftreten, antwortet es mit einem Statuscode, um einen internen Serverfehler zu signalisieren. 500 Kommen wir als Nächstes zum Löschen von Benutzerdaten. Erstellen Sie und fügen Sie diesen Code hinzu: 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); } }; Benutzercontroller löschen Wenn eine Anfrage empfangen wird, extrahiert es die Benutzer-ID aus dem Anfrageobjekt, das normalerweise von der Authentifizierungs-Middleware abgerufen wird. Anschließend wird die mit dieser verknüpfte Sitzung mithilfe des Redis-Clients aus Redis gelöscht. Anschließend ruft es die des auf, um die Daten des Benutzers aus der Datenbank zu entfernen. user_id delete UserModel Nach erfolgreichem Löschen sowohl der Sitzung als auch der Benutzerdaten antwortet es mit dem Statuscode , um den erfolgreichen Löschvorgang anzuzeigen. Tritt während des Löschvorgangs ein Fehler auf, antwortet er mit dem Statuscode , der auf einen internen Serverfehler hinweist. 200 500 Um die Benutzerdaten im System zu aktualisieren, erstellen Sie und fügen Sie der Datei den folgenden Code hinzu: 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); } }; Benutzercontroller aktualisieren Beim Empfang einer Anfrage werden die Felder , und aus dem Anfragetext extrahiert. Anschließend werden diese Felder mithilfe der Dienstprogrammfunktion gefiltert, um sicherzustellen, dass nur gültige Felder in der Nutzlast enthalten sind. first_name last_name username filterObject Anschließend wird geprüft, ob der angegebene bereits in der Datenbank vorhanden ist. Wenn dies der Fall ist, antwortet der Controller mit einem Statuscode und einer Fehlermeldung, die auf einen ungültigen hinweist. Wenn der eindeutig ist, aktualisiert der Controller die Benutzerdaten in der Datenbank mithilfe der Methode des . username 400 username username updateOneById UserModel Bei erfolgreicher Aktualisierung antwortet es mit einem Statuscode und den aktualisierten Benutzerdaten. Treten während des Aktualisierungsvorgangs Fehler auf, antwortet der Controller mit dem Statuscode , um einen internen Serverfehler anzuzeigen. 200 500 Der letzte Schritt besteht darin, das Passwort zu aktualisieren. Dies entspricht im Wesentlichen dem Aktualisieren der Benutzerdaten, jedoch mit Hashing des neuen Passworts. Erstellen Sie den letzten Controller aus unserer Liste und fügen Sie den Code hinzu: 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); } }; Passwort-Controller aktualisieren Beim Empfang einer Anfrage wird das neue Passwort aus dem Anfragetext extrahiert. Anschließend wird geprüft, ob im Anforderungstext ein Kennwort angegeben ist. Wenn nicht, antwortet es mit dem Statuscode , was auf eine fehlerhafte Anfrage hinweist. Als nächstes wird das neue Passwort mithilfe der Bibliothek mit einem Salt-Faktor von 10 gehasht. 400 bcrypt Das gehashte Passwort wird dann mithilfe der Methode des sicher in der Datenbank gespeichert und mit der verknüpft. Bei erfolgreicher Passwortaktualisierung antwortet der Controller mit einem Statuscode und einem JSON-Objekt, das die Benutzer-ID enthält. updateOneById UserModel user.id 200 Bei Fehlern während der Passwortaktualisierung antwortet der Controller mit einem Statuscode, um wie bei anderen Controllern einen internen Serverfehler anzuzeigen. 500 Stellen Sie sicher, dass Sie den Validierungshelfer und die Dienstprogramme von überprüfen und einrichten . Nach der Konfiguration sollten Sie bereit sein, die Endpunkte zu testen. GitHub-Repository Lassen Sie uns den Anmeldeendpunkt überprüfen: Wie ersichtlich, haben wir ein Token erhalten, das im Header zum Abrufen der Sitzung verwendet wird. Wir haben das Autorisierungstoken im Header an den Server gesendet und als Antwort stellte uns der Server die aus der Datenbank abgerufenen Benutzerdaten zur Verfügung. Fühlen Sie sich frei, Sicherheitsfunktionen und Redis-Caching zu erkunden und damit zu experimentieren. Wenn das Grundmodell eingerichtet ist, können Sie sich mit zusätzlichen Funktionen befassen, beispielsweise mit der Kontowiederherstellung für Benutzer, die ihre Passwörter vergessen haben. Dieses Thema bleibt jedoch einem zukünftigen Artikel vorbehalten. Abschluss Die skalierbare Verwaltung des Routing- und Benutzerauthentifizierungsflusses kann eine Herausforderung sein. Während wir Middleware zum Schutz von Routen implementiert haben, stehen zusätzliche Strategien zur Verbesserung der Leistung und Zuverlässigkeit des Dienstes zur Verfügung. Durch die Bereitstellung klarerer Fehlermeldungen wird das Benutzererlebnis weiter verbessert, da die Fehlerbehandlung ein wichtiger Aspekt bleibt, der eine umfassendere Abdeckung erfordert. Wir haben jedoch den primären Authentifizierungsablauf erfolgreich implementiert, der es Benutzern ermöglicht, sich anzumelden, auf ihre Konten zuzugreifen, Sitzungsdaten abzurufen, Benutzerinformationen zu aktualisieren und Konten zu löschen. Ich hoffe, dass Sie diese Reise aufschlussreich fanden und wertvolle Erkenntnisse zur Benutzerauthentifizierung gewonnen haben. Ressourcen GitHub-Repo Knex.js Äußern Knotenanwendung erstellen Briefträger Auch veröffentlicht hier