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.
Bevor ich mich in die Programmierung stürze, möchte ich sicherstellen, dass wir mit derselben Codebasis arbeiten, auf die Sie über meine öffentliche
Sehen Sie sich nun das Schema des Ablaufs bei vergessenem Passwort an.
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.
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
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.
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.
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:
Geben Sie den Namen ein, der verwendet werden soll. In meinem Fall lege ich Nodemailer
fest und drücke 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.
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.
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
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:
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.
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.
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
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.
Hier finden Sie einige Referenzen, die ich in diesem Artikel verwendet habe:
Auch hier erschienen