Nest.js Blog Schritt für Schritt: Autorisierung hinzufügen
Wenhao Wang
Dev Intern · Leapcell

Im vorherigen Tutorial haben wir erfolgreich einen voll funktionsfähigen persönlichen Blog erstellt. Jeder konnte neue Beiträge besuchen und erstellen. Für einen echten persönlichen Blog können jedoch normalerweise nur der Blogger selbst Inhalte veröffentlichen und verwalten.
In diesem Tutorial fügen wir unserem Blog Benutzerauthentifizierungs- und Autorisierungsfunktionen hinzu. Wir werden Folgendes implementieren:
- Benutzerregistrierung und Anmeldefunktionalität.
- Ein Session-Cookie-Mechanismus zur Verwaltung des Benutzeranmeldestatus.
- Sitzungspersistenz mit Redis, um sicherzustellen, dass der Anmeldestatus nach einem Dienstneustart nicht verloren geht.
- Schutz der Beitragerstellungsfunktion, sodass nur angemeldete Benutzer Zugriff haben.
Ohne weitere Umschweife, machen wir weiter.
1. Auswahl einer Authentifizierungsmethode
In der Webentwicklung sind die beiden häufigsten Authentifizierungsmethoden Token-basiert (z. B. JWT) und Sitzungs-basiert (Cookie).
- JWT (JSON Web Tokens): Der Server speichert keinen Benutzerstatus. Nach der Anmeldung eines Benutzers generiert der Server ein verschlüsseltes Token und gibt es an den Client zurück. Der Client fügt dieses Token in nachfolgende Anfragen ein, und der Server validiert seine Legitimität. Diese Methode ist ideal für zustandslose RESTful APIs und Single Page Applications (SPAs).
- Session-Cookie: Nach der Anmeldung eines Benutzers erstellt der Server eine Sitzung und sendet die Sitzungs-ID über ein Cookie an den Browser zurück. Der Browser fügt dieses Cookie automatisch in nachfolgende Anfragen ein. Der Server verwendet die Sitzungs-ID aus dem Cookie, um die entsprechenden Sitzungsinformationen zu finden und so den Benutzer zu identifizieren.
In diesem Tutorial wählen wir die Session-Cookie-Methode. Der Grund dafür ist, dass unser Blog eine traditionelle Anwendung mit serverseitig gerenderten Seiten (EJS) ist. In diesem Modell sind Browser und Server eng gekoppelt, was die cookie-basierte Sitzungsverwaltung zur direktesten, klassischen und sichersten Methode macht. Nest.js bietet auch eine hervorragende integrierte Unterstützung für diesen Ansatz.
2. Erstellen des Benutzer Moduls
Ähnlich wie wir das posts
-Modul im vorherigen Tutorial erstellt haben, benötigen wir zunächst ein Modul zur Verwaltung von Benutzern.
Verwenden Sie die Nest CLI, um schnell die notwendigen Dateien zu generieren:
nest generate module users nest generate controller users nest generate service users
Erstellen Sie als Nächstes eine Benutzerentitätsdatei user.entity.ts
im Verzeichnis src/users
, um die users
-Tabelle in der Datenbank abzubilden.
// src/users/user.entity.ts import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) username: string; @Column() password: string; // Das gespeicherte Passwort ist der verschlüsselte Hash }
Registrieren Sie dann das TypeOrmModule
im UsersModule
(src/users/users.module.ts
), damit es auf die User
-Entität zugreifen kann.
// src/users/users.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], exports: [UsersService], // Exportieren Sie UsersService zur Verwendung in anderen Modulen }) export class UsersModule {}
Führen Sie schließlich die folgende SQL-Anweisung in Leapcell oder Ihrer lokalen PostgreSQL-Datenbank aus, um die user
-Tabelle zu erstellen:
CREATE TABLE "user" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "username" VARCHAR UNIQUE NOT NULL, "password" VARCHAR NOT NULL );
3. Registrierung und Passwortverschlüsselung implementieren
Benutzerpasswörter dürfen niemals im Klartext in der Datenbank gespeichert werden. Wir verwenden die bcrypt
-Bibliothek, um Passwörter zu hashen und zu verschlüsseln.
Installieren Sie zunächst die erforderlichen Abhängigkeiten:
npm install bcrypt npm install -D @types/bcrypt
Aktualisieren Sie als Nächstes src/users/users.service.ts
, um die Logik zum Erstellen und Finden von Benutzern hinzuzufügen.
// src/users/users.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User> ) {} async create(user: Partial<User>): Promise<User> { const saltRounds = 10; const hashedPassword = await bcrypt.hash(user.password || '', saltRounds); const newUser = this.usersRepository.create({ username: user.username, password: hashedPassword, }); return this.usersRepository.save(newUser); } async findOne(username: string): Promise<User | null> { return this.usersRepository.findOneBy({ username }); } }
4. Konfiguration von Session und Passport.js
Um die Anmeldelogik und die Sitzungsverwaltung zu handhaben, verwenden wir passport
, eine beliebte Node.js-Authentifizierungsbibliothek, zusammen mit express-session
zur Verwaltung von Sitzungen.
Installieren Sie die zugehörigen Abhängigkeiten:
npm install @nestjs/passport passport passport-local express-session npm install -D @types/passport-local @types/express-session
Erstellen des Auth Moduls
Wir werden die gesamte authentifizierungsbezogene Logik (wie Anmeldung, Abmeldung, Strategien) in ein separates auth
-Modul verschieben.
nest generate module auth nest generate service auth
Ändern Sie src/auth/auth.service.ts
, um die Logik zur Validierung von Benutzerpasswörtern hinzuzufügen.
// src/auth/auth.service.ts import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor(private usersService: UsersService) {} async validateUser(username: string, pass: string): Promise<any> { const user = await this.usersService.findOne(username); if (user && (await bcrypt.compare(pass, user.password))) { const { password, ...result } = user; return result; // Validierung erfolgreich, geben Sie das Benutzerobjekt ohne das Passwort zurück } return null; // Validierung fehlgeschlagen } }
Implementieren der Passport Local Strategy
Passport verwendet "Strategien", um verschiedene Anmeldemethoden zu handhaben. Wir werden eine "local strategy" implementieren, die Benutzername und Passwort für die Anmeldung verwendet.
Erstellen Sie local.strategy.ts
im Verzeichnis src/auth
:
// src/auth/local.strategy.ts import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super(); } async validate(username: string, password: string): Promise<any> { const user = await this.authService.validateUser(username, password); if (!user) { throw new UnauthorizedException(); } return user; } }
Konfigurieren des Auth Moduls
Nun fügen wir den gerade erstellten Dienst und die Strategie in auth.module.ts
zusammen.
// src/auth/auth.module.ts import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { LocalStrategy } from './local.strategy'; @Module({ imports: [UsersModule, PassportModule], providers: [AuthService, LocalStrategy], }) export class AuthModule {}
5. Verwenden von Redis zum Speichern von Sitzungen
"expres`s-session" speichert standardmäßig Sitzungen im Arbeitsspeicher des Servers. Das bedeutet, dass alle Benutzeranmeldungsstatus verloren gehen, wenn der Server neu gestartet wird. Um dies zu lösen, verwenden wir Redis, eine Hochleistungs-In-Memory-Datenbank, um Sitzungen persistent zu machen.
Was tun, wenn Sie kein Redis haben? Wenn Sie derzeit keinen Redis-Dienst verfügbar haben, können Sie diesen Schritt überspringen.
express-session
greift automatisch auf die Speicherverwendung zurück. Dies ist für die lokale Entwicklung vollkommen in Ordnung, aber denken Sie daran, dass es für eine Produktionsumgebung nicht geeignet ist.
Installieren Sie die Redis-bezogenen Abhängigkeiten:
npm install redis connect-redis
Öffnen Sie nun die Datei src/main.ts
, um die Sitzungs-Middleware zu konfigurieren und eine Verbindung zu Redis herzustellen.
// src/main.ts import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; import { AppModule } from './app.module'; import * as session from 'express-session'; import * as passport from 'passport'; import { createClient } from 'redis'; import RedisStore from 'connect-redis'; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); // Redis Client initialisieren const redisClient = createClient({ // Wenn Ihr Redis ein Passwort hat oder auf einem anderen Host liegt, ändern Sie die Konfiguration hier // url: 'redis://:password@hostname:port' url: 'redis://localhost:6379', }); await redisClient.connect().catch(console.error); // RedisStore initialisieren const redisStore = new RedisStore({ client: redisClient, prefix: 'blog-session:', }); app.use( session({ store: redisStore, // Redis für die Speicherung verwenden secret: 'your-secret-key', // Ersetzen Sie dies durch eine zufällige, komplexe Zeichenfolge resave: false, saveUninitialized: false, cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 7, // 7 Tage }, }) ); app.use(passport.initialize()); app.use(passport.session()); // Passport Sitzungs-Serialisierung und -Deserialisierung // Nach erfolgreicher Anmeldung werden die Benutzerinformationen serialisiert und in der Sitzung gespeichert passport.serializeUser((user: any, done) => { done(null, user.id); }); // Bei jeder Anfrage findet Passport den Benutzer über die ID in der Sitzung und deserialisiert ihn passport.deserializeUser(async (id: string, done) => { // Hier benötigen Sie eine Methode, um einen Benutzer nach ID zu finden, die wir später zu UsersService hinzufügen werden // const user = await usersService.findById(id); // Der Einfachheit halber geben wir vorerst ein Objekt mit nur der ID zurück done(null, { id }); }); app.useStaticAssets(join(__dirname, '..', 'public')); app.setBaseViewsDir(join(__dirname, '..', 'views')); app.setViewEngine('ejs'); await app.listen(3000); } bootstrap();
Hinweis: Damit deserializeUser
korrekt funktioniert, müssen Sie Ihrer UsersService
eine findById
-Methode hinzufügen.
6. Erstellen von Anmelde- und Registrierungsseiten
Wir müssen eine Benutzeroberfläche für die Benutzer bereitstellen. Erstellen Sie im Verzeichnis views
die Dateien login.ejs
undregister.ejs
.
-
register.ejs
<%- include('_header', { title: 'Register' }) %> <form action="/users/register" method="POST" class="post-form"> <h2>Register</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Register</button> </form> <%- include('_footer') %>
-
login.ejs
<%- include('_header', { title: 'Login' }) %> <form action="/auth/login" method="POST" class="post-form"> <h2>Login</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Login</button> </form> <%- include('_footer') %>
7. Implementieren von Routen und Controller-Logik
Erstellen wir nun die Routen zur Behandlung von Registrierungs- und Anmeldeanfragen.
Registrierungsroute
Aktualisieren Sie src/users/users.controller.ts
:
// src/users/users.controller.ts import { Controller, Get, Post, Render, Body, Res } from '@nestjs/common'; import { UsersService } from './users.service'; import { Response } from 'express'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get('register') @Render('register') showRegisterForm() { return; } @Post('register') async register(@Body() body: any, @Res() res: Response) { // Der Einfachheit halber werden wir hier keine komplexe Validierung durchführen await this.usersService.create(body); res.redirect('/auth/login'); // Nach erfolgreicher Registrierung zur Anmeldeseite umleiten } }
Anmelde- und Abmeldungsrouten
Erstellen Sie einen neuen auth.controller.ts
zur Behandlung von An- und Abmeldungen.
nest generate controller auth
Bearbeiten Sie src/auth/auth.controller.ts
:
// src/auth/auth.controller.ts import { Controller, Get, Post, Render, Request, Res, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Response } from 'express'; @Controller('auth') export class AuthController { @Get('login') @Render('login') showLoginForm() { return; } @UseGuards(AuthGuard('local')) @Post('login') async login(@Request() req, @Res() res: Response) { // Nach erfolgreicher Validierung hängt Passport das Benutzerobjekt an req.user an res.redirect('/posts'); } @Get('logout') logout(@Request() req, @Res() res: Response) { req.logout((err) => { if (err) { console.log(err); } res.redirect('/'); }); } }
Denken Sie schließlich daran, UsersModule
und AuthModule
in app.module.ts
zu importieren.
// src/app.module.ts // ... imports import { UsersModule } from './users/users.module'; import { AuthModule } from './auth/auth.module'; @Module({ imports: [ // ... TypeOrmModule.forRoot(...) PostsModule, UsersModule, AuthModule, ], // ... }) export class AppModule {}
8. Routen schützen und die Benutzeroberfläche aktualisieren
Nachdem wir nun einen Anmeldemechanismus haben, ist der letzte Schritt, ihn zu verwenden, um unsere "Create Post"-Funktion zu schützen.
Erstellen eines Authentifizierungs-Guards
Ein Guard ist eine Klasse in Nest.js, die bestimmt, ob eine gegebene Anfrage behandelt werden soll. Wir erstellen einen AuthenticatedGuard
, um zu überprüfen, ob ein Benutzer angemeldet ist.
Erstellen Sie authenticated.guard.ts
im Verzeichnis src/auth
:
// src/auth/authenticated.guard.ts import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; @Injectable() export class AuthenticatedGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); return request.isAuthenticated(); // Von Passport hinzugefügte Methode, prüft, ob ein Benutzer in der Sitzung vorhanden ist } }
Anwenden des Guards
Öffnen Sie src/posts/posts.controller.ts
und wenden Sie den @UseGuards(AuthenticatedGuard)
-Decorator auf die Routen an, die geschützt werden müssen.
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Post, Body, Res, UseGuards } from '@nestjs/common'; import { AuthenticatedGuard } from 'src/auth/authenticated.guard'; // ...andere Imports @Controller('posts') export class PostsController { // ... constructor // ... findAll() @UseGuards(AuthenticatedGuard) // <--- Guard anwenden @Get('new') @Render('new-post') newPostForm() { return; } @UseGuards(AuthenticatedGuard) // <--- Guard anwenden @Post() async create(@Body() body: { title: string; content: string }, @Res() res: Response) { await this.postsService.create(body); res.redirect('/posts'); } // ... findOne() }
Wenn nun ein nicht angemeldeter Benutzer versucht, auf /posts/new
zuzugreifen, wird er automatisch abgefangen.
Aktualisieren der Frontend-Benutzeroberfläche
Schließlich aktualisieren wir die Benutzeroberfläche, um unterschiedliche Schaltflächen basierend auf dem Anmeldestatus des Benutzers anzuzeigen. Dazu müssen wir den Anmeldestatus des Benutzers an das Frontend übergeben, wenn EJS-Vorlagen gerendert werden. Dies erfordert eine kleine Änderung der Controller app.controller.ts
und posts.controller.ts
.
Eine einfache Möglichkeit besteht darin, req.user
manuell in jeder Route, die @Render
verwendet, zu übergeben.
Aktualisieren Sie _header.ejs
, um Links für Anmeldung/Registrierung und Abmeldung/neuen Beitrag hinzuzufügen.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title><%= title %></title> <link rel="stylesheet" href="/css/style.css" /> </head> <body> <header> <h1><a href="/">My Blog</a></h1> <div class="user-actions"> <% if (user) { %> <span>Welcome, <%= user.username || 'User' %></span> <a href="/posts/new" class="new-post-btn">New Post</a> <a href="/auth/logout">Logout</a> <% } else { %> <a href="/auth/login">Login</a> <a href="/users/register">Register</a> <% } %> </div> </header> <main></main> </body> </html>
Wir müssen unsere Controller aktualisieren, um die user
-Informationen an die Ansichten zu übergeben. In der root
-Methode von posts.controller.ts
:
// src/posts/posts.controller.ts // ... import { Request } from 'express'; // Request importieren @Controller('posts') export class PostsController { // ... @Get() @Render('index') async root(@Request() req) { // Das Request-Objekt injizieren const posts = await this.postsService.findAll(); return { posts, user: req.user }; // Das Benutzerobjekt an die Vorlage übergeben } // ...machen Sie ähnliche Änderungen an anderen @Render-Routen @Get(':id') @Render('post') async post(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); return { post, user: req.user }; } }
9. Ausführen und Testen
Starten Sie nun Ihre Anwendung neu:
npm run start:dev
Besuchen Sie http://localhost:3000
.
- Sie sehen rechts oben die Schaltflächen "Login" und "Register".
- Klicken Sie auf "Registreiren" und erstellen Sie einen neuen Benutzer.
- Nach erfolgreicher Registrierung werden Sie zur Anmeldeseite weitergeleitet. Melden Sie sich mit dem gerade erstellten Konto an.
- Nach erfolgreicher Anmeldung ändern sich die Schaltflächen oben rechts zu "New Post" und "Logout". Sie können jetzt neue Beiträge erstellen.
An diesem Punkt haben wir erfolgreich ein vollständiges Benutzerauthentifizierungssystem zu unserem Blog hinzugefügt.