paint-brush
Ein Tutorial zum Kontowiederherstellungsprozess mit NodeJS mit Knex und Expressvon@antonkalik
659 Lesungen
659 Lesungen

Ein Tutorial zum Kontowiederherstellungsprozess mit NodeJS mit Knex und Express

von Anton Kalik12m2024/03/28
Read on Terminal Reader

Zu lang; Lesen

In dieser Anleitung wird das Zurücksetzen von Benutzerkennwörtern mit Nodemailer, Knex und Express erläutert. Es umfasst das Versenden von E-Mails zur Passwortwiederherstellung und die Validierung der Nachrichtenzustellung.
featured image - Ein Tutorial zum Kontowiederherstellungsprozess mit NodeJS mit Knex und Express
Anton Kalik HackerNoon profile picture
0-item

Dies ist eine detaillierte Analyse, wie ein Reset für einen Benutzer durchgeführt wird, wenn er sein Passwort vergessen hat, und wie E-Mails von Node JS gesendet und das Senden von Nachrichten validiert werden.

Die meisten von uns haben den Vorgang der Kontowiederherstellung mindestens einmal erlebt – wenn wir ein Passwort vergessen, müssen wir Verfahren entwickeln, um ein neues Passwort zu erstellen und wieder Zugriff auf das System zu erhalten. Dieser Artikel konzentriert sich auf die Implementierung eines solchen Vorgangs mit Node.js, Knex und einigen nicht genannten Tools sowie Express, um Routen zu verwalten und die erforderlichen Vorgänge auszuführen.


Wir behandeln die Routerimplementierung, den Umgang mit URL-Parametern, die Festlegung, was dem Benutzer gesendet werden soll, wenn nur eine E-Mail-Adresse oder Telefonnummer als Nachweis verfügbar ist, die Verwaltung von E-Mail-Einreichungen und die Behandlung von Sicherheitsbedenken.

Ablauf zum Vergessen des Passworts

Bevor ich mich in die Programmierung stürze, möchte ich sicherstellen, dass wir mit derselben Codebasis arbeiten, auf die Sie über meine öffentliche Repository auf GitHub . Wir führen die Aktualisierung Schritt für Schritt durch, um den Ablauf bei vergessenen Passwörtern zu implementieren. Für den E-Mail-Transport nutzen wir den E-Mail-Dienst von Google.


Sehen Sie sich nun das Schema des Ablaufs bei vergessenem Passwort an.

Flussschema für vergessenes Passwort

Der Server ist dafür verantwortlich, E-Mails mit einem gültigen Link zum Zurücksetzen des Kennworts an das Postfach des Benutzers zu senden. Darüber hinaus überprüft er das Token und die Existenz des Benutzers.

Pakete und Migration

Um den E-Mail-Dienst zu nutzen und E-Mails mit Node.js zu versenden, müssen wir zusätzlich zu unseren bestehenden Abhängigkeiten die folgenden Pakete installieren:

 npm i --save nodemailer handlebars


Nodemailer : Leistungsstarkes Modul, das das einfache Senden von E-Mails über SMTP oder andere Transportmechanismen ermöglicht.


Handlebars : Handlebars ist eine beliebte Template-Engine für JavaScript. Sie ermöglicht es uns, Templates mit Platzhaltern zu definieren, die beim Rendern mit Daten gefüllt werden können.


Jetzt müssen wir die Migration erstellen. In meinem Fall muss ich der users eine neue Spalte forgot_password_token hinzufügen:

 knex migrate:make add_field_forgot_password_token -x ts


und in der generierten Datei habe ich den Code eingefügt:

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

Migration für „Kennwort vergessen“-Token in der Benutzertabelle


und migrieren Sie dann die neueste Datei:

 knex migrate:knex

So, jetzt können wir in der users unser forgot_password_token festlegen

Router

Um Controller zu verwalten, die für die Handhabung der Logik bei vergessenen Passwörtern und deren Zurücksetzung verantwortlich sind, müssen wir zwei Routen einrichten. Die erste Route leitet den Vorgang bei vergessenen Passwörtern ein, während die zweite den Vorgang zum Zurücksetzen behandelt und dabei einen Token-Parameter in der URL zur Überprüfung erwartet. Um dies zu implementieren, erstellen Sie eine Datei mit dem Namen forgotPasswordRouter.ts im Verzeichnis src/routes/ und fügen den folgenden Code ein:

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

Router-Passwort vergessen


Zwei Controller verwalten die Logik zum Versenden von E-Mails und zum Zurücksetzen des Passworts.

Passwort vergessen Controller

Wenn der Client sein Passwort vergisst, hat er keine Sitzung, was bedeutet, dass wir keine Benutzerdaten außer E-Mail oder anderen Sicherheitskennungen erhalten können. In unserem Fall senden wir eine E-Mail, um eine Passwortzurücksetzung durchzuführen. Diese Logik werden wir in den Controller einbauen.

 forgotPasswordRouter.post('/', forgotPasswordController);


Erinnern Sie sich an den Link „Passwort vergessen?“ unter dem Anmeldeformular, der sich normalerweise in der Benutzeroberfläche aller Clients im Anmeldeformular befindet? Wenn wir darauf klicken, gelangen wir zu einer Ansicht, in der wir eine Kennwortzurücksetzung anfordern können. Wir geben einfach unsere E-Mail-Adresse ein und der Controller übernimmt alle erforderlichen Vorgänge. Sehen wir uns den folgenden Code an:


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

Passwort vergessen Controller


Aus dem Textkörper erhalten wir eine E-Mail und finden den Benutzer dann mit UserModel.findByEmail . Wenn der Benutzer existiert, erstellen wir mit TokenService.sign ein JWT-Token und speichern das Token mit einer Gültigkeit von 1 Tag im Benutzer forgot_password_token . Dann senden wir die Nachricht mit einem entsprechenden Link zusammen mit einem Token an die E-Mail, wo der Benutzer sein Passwort ändern kann.

Google-Einrichtung

Um die E-Mail senden zu können, müssen wir unsere neue E-Mail-Adresse erstellen, die als Absender dient.


Gehen wir zu Google, um ein neues E-Mail-Konto zu erstellen. Wenn das Konto erstellt ist, fahren Sie mit dem Link „Google-Konto verwalten“ fort. Sie finden ihn oben rechts, indem Sie auf Avatar klicken. Klicken Sie dann im linken Menü auf das Element „Sicherheit “ und drücken Sie dann auf „Bestätigung in zwei Schritten“ . Unten finden Sie den Abschnitt „App-Passwörter“ . Klicken Sie auf den Pfeil:


App-Passwörter

Geben Sie den Namen ein, der verwendet werden soll. In meinem Fall lege ich Nodemailer fest und drücke Erstellen .

App-Passwort erstellen

Kopieren Sie das generierte Passwort und legen Sie es in Ihrer .env Datei fest. Wir müssen in der Datei zwei Variablen festlegen:

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


Um eine richtige E-Mail-Adresse wie info@company_name.com zu haben, müssen Sie natürlich Google Workspace oder AWS Amazon WorkMail zusammen mit AWS SES oder anderen Diensten einrichten. In unserem Fall verwenden wir jedoch ein einfaches, kostenloses Gmail-Konto.

E-Mail-Dienst

Nachdem wir die .env Datei vorbereitet haben, können wir unseren Dienst zum Senden von E-Mails einrichten. Der Controller verwendet den Dienst mit dem generierten Token und der E-Mail-Adresse des Empfängers für unsere Nachricht.

 await EmailService.sendPasswordResetEmail(email, token);


Erstellen wir src/services/EmailService.ts und definieren die Klasse für den Dienst:

 export class EmailService {}


Und jetzt muss ich als Ausgangsdaten die Umgebung für die Verwendung mit nodemailer erstellen:

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

E-Mail-Dienst


Wir müssen uns um die Initialisierung des Dienstes kümmern. Ich habe bereits in meinem vorherigen Artikel darüber geschrieben. Artikel . Hier ist ein Beispiel:

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

Dienste initialisieren


Fahren wir nun mit der Erstellung der Initialisierung innerhalb unserer EmailService Klasse fort:

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

Initialisierung des E-Mail-Dienstes


Es gibt die Initialisierungsmethode nodemailer.createTransport() , die von der nodemailer Bibliothek bereitgestellt wird. Sie erstellt ein Transporterobjekt, das zum Senden unserer E-Mails verwendet wird. Die Methode akzeptiert ein Optionsobjekt als Argument, in dem Sie die Konfigurationsdetails für den Transporter angeben.


Wir verwenden Google: service: 'gmail' gibt den E-Mail-Dienstanbieter an. Nodemailer bietet integrierte Unterstützung für verschiedene E-Mail-Dienstanbieter, und gmail gibt an, dass der Transporter für die Arbeit mit dem SMTP-Server von Gmail konfiguriert wird.


Für die Authentifizierung auth müssen die Anmeldeinformationen festgelegt werden, die für den Zugriff auf den SMTP-Server des E-Mail-Dienstanbieters erforderlich sind.


Für user sollte die E-Mail-Adresse eingestellt werden, von der wir E-Mails senden werden, und das Passwort sollte im Google-Konto von App Passwords generiert worden sein.


Lassen Sie uns nun den letzten Teil unseres Dienstes festlegen:

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

E-Mail zum Zurücksetzen des Passworts senden


Bevor Sie fortfahren, müssen Sie unbedingt den entsprechenden Host bestimmen, wenn der Client eine E-Mail erhält. Es ist wichtig, einen Link mit einem Token im E-Mail-Text einzurichten.


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

Host abrufen


Als Vorlagen verwende ich handlebars und dafür müssen wir in src/temlates/passwordResetTemplate.hbs unsere erste HTML-Vorlage erstellen:


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

Vorlage zum Zurücksetzen des Passworts


und jetzt können wir diese Vorlage mit dem Helfer wiederverwenden:

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

Vorlagen-Helfer generieren


Um unsere E-Mail zu verbessern, können wir sogar Anhänge hinzufügen. Fügen Sie dazu die Datei email_logo.png zum Ordner src/assets hinzu. Wir können dieses Bild dann mithilfe der folgenden Hilfsfunktion in der E-Mail rendern:


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

Helfer zum Generieren von Anhängen


Nachdem wir alle diese Helfer gesammelt haben, müssen wir in der Lage sein, E-Mails mit Folgendem zu versenden:

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


Dieser Ansatz bietet eine angemessene Skalierbarkeit und ermöglicht es dem Dienst, verschiedene Methoden zum Senden von E-Mails mit unterschiedlichem Inhalt zu verwenden.


Versuchen wir nun, den Controller mit unserem Router auszulösen und die E-Mail zu senden. Dafür verwende ich Briefträger :

E-Mail von Postman senden

Die Konsole teilt Ihnen mit, dass die Nachricht gesendet wurde:

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


Prüfen Sie, ob im Posteingang neue Nachrichten vorhanden sind:

Posteingang mit einer neuen E-Mail vom Server

Der Link zum Zurücksetzen des Passworts muss das Token und den Host enthalten:

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


Der Port 3000 wird hier angegeben, da diese Nachricht den Entwicklungsprozess betrifft. Dies zeigt an, dass der Client, der für die Handhabung der Formulare zum Zurücksetzen des Passworts zuständig ist, auch innerhalb der Entwicklungsumgebung ausgeführt wird.

Passwort zurücksetzen

Das Token muss auf der Controllerseite mit TokenService validiert werden, wo wir den Benutzer ermitteln können, der diese E-Mail gesendet hat. Lassen Sie uns den Router wiederherstellen, der das Token verwendet:

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


Der Controller aktualisiert das Passwort nur, wenn das Token gültig ist und nicht abgelaufen ist (die Ablaufzeit beträgt eine Stunde). Um diese Funktion zu implementieren, navigieren Sie zum Ordner src/controllers/ und erstellen Sie eine Datei mit dem Namen resetPasswordController.ts , die den folgenden Code enthält:

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

Kennwort-Controller zurücksetzen


Dieser Controller empfängt das Token, überprüft es, extrahiert die Benutzer-ID aus den entschlüsselten Daten, ruft den entsprechenden Benutzer ab, holt das vom Client im Anforderungstext gesendete neue Kennwort ab und aktualisiert das Kennwort in der Datenbank. Dadurch kann sich der Client schließlich mit dem neuen Kennwort anmelden.

Abschluss

Die Skalierbarkeit des E-Mail-Dienstes wird durch verschiedene Ansätze demonstriert, wie das Senden von Bestätigungen oder Erfolgsmeldungen, etwa solche, die auf eine Kennwortaktualisierung hinweisen und eine nachfolgende Anmeldung ermöglichen. Die Verwaltung von Kennwörtern ist jedoch eine große Herausforderung, insbesondere wenn die Verbesserung der Anwendungssicherheit unabdingbar ist.


Es stehen zahlreiche Optionen zur Verbesserung der Sicherheit zur Verfügung, darunter zusätzliche Prüfungen vor der Zulassung von Kennwortänderungen, etwa Token-Vergleich, E-Mail und Kennwortvalidierung.


Eine weitere Möglichkeit ist die Implementierung eines PIN-Code-Systems, bei dem ein Code zur Validierung auf der Serverseite an die E-Mail des Benutzers gesendet wird. Jede dieser Maßnahmen erfordert die Nutzung von E-Mail-Versandfunktionen.


Den gesamten implementierten Code finden Sie im GitHub-Repository hier .


Bitte führen Sie mit diesem Build beliebige Experimente durch und teilen Sie uns Ihr Feedback dazu mit, welche Aspekte Sie an diesem Thema schätzen. Vielen Dank.

Verweise

Hier finden Sie einige Referenzen, die ich in diesem Artikel verwendet habe:


Auch hier erschienen