Middleware, Pipes i Guards

NestJS oferuje wiele narzędzi, które pomagają budować zaawansowane i bezpieczne aplikacje. Wśród nich warto wyróżnić middleware, pipes oraz guards. W dzisiejszym wpisie na bloga wyjaśnimy, czym są te pojęcia, jak działają i jak mogą pomóc w tworzeniu wydajnych i bezpiecznych aplikacji. Dowiemy się, jak tworzyć middleware do logowania i autoryzacji, pipes do walidacji danych oraz guards do ochrony endpointów.

Middleware w NestJS

Czym jest middleware?

Middleware to funkcje, które są wykonywane przed przetwarzaniem właściwego żądania przez kontrolery. Działa podobnie jak middleware w innych frameworkach, takich jak Express.js – ma dostęp do obiektu żądania (req), odpowiedzi (res) i może wprowadzać zmiany, zanim żądanie trafi do dalszej obsługi. Middleware jest używane do różnych celów, takich jak logowanie, autoryzacja, obsługa ciasteczek, a nawet modyfikowanie żądań.

Tworzenie middleware w NestJS

Poniżej zaprezentowano przykład prostego middleware do logowania:

				
					import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    next();
  }
}

				
			

Powyższy kod to klasyczne middleware do logowania, które wyświetla w konsoli metodę HTTP oraz adres URL każdego przychodzącego żądania.

Rejestracja middleware

Aby użyć naszego middleware, musimy je zarejestrować. W AppModule możemy to zrobić w metodzie configure():

				
					import { Module, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { UzytkownikModule } from './uzytkownik/uzytkownik.module';

@Module({
  imports: [UzytkownikModule],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*'); // lub .forRoutes(UzytkownikController)
  }
}

				
			

W ten sposób LoggerMiddleware zostanie wywołane dla wszystkich tras ('*'). Możemy także ograniczyć jego użycie do konkretnych tras lub kontrolerów.

Middleware do autoryzacji

Tworzenie middleware do autoryzacji polega na sprawdzaniu, czy użytkownik ma dostęp do konkretnej trasy na podstawie nagłówka (np. tokenu JWT):

				
					import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers['authorization'];

    if (!token || token !== 'secret-token') {
      throw new UnauthorizedException('Brak odpowiednich uprawnień');
    }

    next();
  }
}

				
			

Powyższe middleware sprawdza, czy nagłówek authorization zawiera token. Jeśli nie, rzuca wyjątek UnauthorizedException, który zwróci odpowiedź z kodem 401.

Pipes i walidacja

Czym są pipes?

Pipes w NestJS to narzędzia służące do transformacji i walidacji danych, zanim trafią one do kontrolera. Pipes mogą być używane do przekształcania wartości (np. zmiana typu) oraz walidacji, a jeśli walidacja nie powiedzie się, to automatycznie zgłaszają błąd, co powoduje przerwanie przetwarzania żądania.

Wbudowane pipes

NestJS dostarcza kilka wbudowanych pipes, takich jak ValidationPipe, który jest używany do walidacji danych. Przykład poniżej pokazuje, jak używać ValidationPipe w kontrolerze:

				
					import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { StworzUzytkownikDto } from './dto/stworz-uzytkownik.dto';

@Controller('uzytkownicy')
export class UzytkownikController {
  @Post()
  @UsePipes(ValidationPipe)
  stworzUzytkownika(@Body() stworzUzytkownikDto: StworzUzytkownikDto) {
    // kod do tworzenia użytkownika
  }
}

				
			

Dzięki użyciu @UsePipes(ValidationPipe) wszystkie dane przesłane w ciele żądania (@Body()) zostaną zwalidowane przed przekazaniem do metody kontrolera.

Aby walidacja działała, musimy utworzyć DTO (Data Transfer Object) z odpowiednimi dekoratorami walidacji:

				
					import { IsString, IsEmail, MinLength } from 'class-validator';

export class StworzUzytkownikDto {
  @IsString()
  imie: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  haslo: string;
}

				
			

Tworzenie własnych pipes

Możemy również tworzyć własne pipes do bardziej specyficznych przypadków. Przykładem może być pipe, który przekształca dane na liczbę:

				
					import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const parsedValue = parseInt(value, 10);
    if (isNaN(parsedValue)) {
      throw new BadRequestException('Nieprawidłowy numer');
    }
    return parsedValue;
  }
}

				
			

Nasz pipe ParseIntPipe zamienia wartość z ciągu znaków na liczbę. Jeśli konwersja się nie powiedzie, zgłasza wyjątek BadRequestException.

Guards i autoryzacja

Czym są guards?

Guards są mechanizmami, które decydują, czy dany użytkownik ma dostęp do określonego zasobu lub endpointu. Są one wykonywane przed middleware, więc mogą chronić przed wykonaniem nawet samego middleware, jeśli użytkownik nie spełnia określonych warunków dostępu.

Tworzenie guards

Przykład prostego guard:

				
					import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers['authorization'];

    if (!token || token !== 'secret-token') {
      throw new UnauthorizedException('Brak odpowiednich uprawnień');
    }

    return true;
  }
}

				
			

Guard AuthGuard działa podobnie do middleware do autoryzacji – sprawdza, czy nagłówek authorization zawiera odpowiedni token, a jeśli nie, rzuca wyjątek UnauthorizedException.

Używanie guards w kontrolerach

Aby użyć guard, możemy skorzystać z dekoratora @UseGuards():

				
					import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('uzytkownicy')
export class UzytkownikController {
  @Get()
  @UseGuards(AuthGuard)
  znajdzWszystkich() {
    // kod do znajdowania wszystkich użytkowników
  }
}

				
			

Guard AuthGuard zostanie użyty do ochrony endpointu GET /uzytkownicy, dzięki czemu tylko użytkownicy z odpowiednim tokenem będą mogli uzyskać dostęp.

Dekoratory @Roles() i rola guards

Możemy także tworzyć guards do autoryzacji użytkowników na podstawie ich ról. Najpierw stworzymy dekorator @Roles():

				
					import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

				
			

Następnie tworzymy guard RolesGuard:

				
					import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

				
			

Guard RolesGuard sprawdza, czy użytkownik posiada odpowiednią rolę, aby uzyskać dostęp do danego endpointu.

Możemy teraz użyć dekoratora @Roles() do ochrony endpointów:

				
					import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';

@Controller('uzytkownicy')
@UseGuards(RolesGuard)
export class UzytkownikController {
  @Get('admin')
  @Roles('admin')
  znajdzAdminow() {
    // kod do znajdowania adminów
  }
}

				
			

Powyższy endpoint GET /uzytkownicy/admin jest chroniony i dostępny tylko dla użytkowników z rolą admin.

Podsumowanie

W tym wpisie omówiliśmy middleware, pipes oraz guards w NestJS, które są niezwykle przydatne do budowania skalowalnych, bezpiecznych aplikacji. Middleware pozwala przetwarzać żądania przed ich trafieniem do kontrolera, pipes służą do walidacji i transformacji danych, a guards decydują o tym, czy dany użytkownik ma dostęp do konkretnego zasobu.

Każdy z tych komponentów jest potężnym narzędziem, które zwiększa elastyczność i bezpieczeństwo aplikacji. Pracując z middleware, pipes i guards, możesz tworzyć aplikacje, które są nie tylko bardziej bezpieczne, ale również lepiej zorganizowane i łatwiejsze do utrzymania. NestJS dostarcza gotowe narzędzia, dzięki którym możesz szybko wdrożyć zaawansowane mechanizmy autoryzacji, walidacji i logowania.